Add summary section with product breakdown
- Show total volume, total amount, and average price per product - Include grand total row - Products sorted alphabetically
This commit is contained in:
58
src/main.rs
58
src/main.rs
@@ -1,5 +1,6 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -12,6 +13,21 @@ fn fmt(v: f64) -> String {
|
|||||||
format!("{:.2}", v)
|
format!("{:.2}", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ProductSummary {
|
||||||
|
name: String,
|
||||||
|
volume: String,
|
||||||
|
amount: String,
|
||||||
|
avg_price: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Summary {
|
||||||
|
total_volume: String,
|
||||||
|
grand_total: String,
|
||||||
|
products: Vec<ProductSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CardData {
|
struct CardData {
|
||||||
card_number: String,
|
card_number: String,
|
||||||
@@ -34,7 +50,7 @@ struct FormattedTransaction {
|
|||||||
struct PreparedCustomer {
|
struct PreparedCustomer {
|
||||||
customer_number: String,
|
customer_number: String,
|
||||||
cards: Vec<CardData>,
|
cards: Vec<CardData>,
|
||||||
grand_total: String,
|
summary: Summary,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedCustomer {
|
impl PreparedCustomer {
|
||||||
@@ -78,10 +94,48 @@ impl PreparedCustomer {
|
|||||||
.map(|c| c.total_amount.parse::<f64>().unwrap())
|
.map(|c| c.total_amount.parse::<f64>().unwrap())
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
|
let mut product_totals: HashMap<String, (f64, f64)> = HashMap::new();
|
||||||
|
for card in &cards {
|
||||||
|
for tx in &card.transactions {
|
||||||
|
let volume: f64 = tx.volume.parse().unwrap();
|
||||||
|
let amount: f64 = tx.amount.parse().unwrap();
|
||||||
|
let entry = product_totals
|
||||||
|
.entry(tx.quality_name.clone())
|
||||||
|
.or_insert((0.0, 0.0));
|
||||||
|
entry.0 += volume;
|
||||||
|
entry.1 += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut products: Vec<ProductSummary> = product_totals
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, (volume, amount))| {
|
||||||
|
let avg_price = if volume > 0.0 { amount / volume } else { 0.0 };
|
||||||
|
ProductSummary {
|
||||||
|
name,
|
||||||
|
volume: fmt(volume),
|
||||||
|
amount: fmt(amount),
|
||||||
|
avg_price: fmt(avg_price),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
products.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
let total_volume: f64 = products
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.volume.parse::<f64>().unwrap())
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let summary = Summary {
|
||||||
|
total_volume: fmt(total_volume),
|
||||||
|
grand_total: fmt(grand_total),
|
||||||
|
products,
|
||||||
|
};
|
||||||
|
|
||||||
PreparedCustomer {
|
PreparedCustomer {
|
||||||
customer_number: customer.customer_number,
|
customer_number: customer.customer_number,
|
||||||
cards,
|
cards,
|
||||||
grand_total: fmt(grand_total),
|
summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,40 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
.summary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.summary h2 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.summary-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.summary-table th, .summary-table td {
|
||||||
|
padding: 5px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.summary-table th {
|
||||||
|
background: #e0e0e0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.summary-table td:not(:first-child),
|
||||||
|
.summary-table th:not(:first-child) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.grand-total-row td {
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: 2px solid #333;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
.card-section {
|
.card-section {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
@@ -111,6 +145,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>Sammanfattning</h2>
|
||||||
|
<table class="summary-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Produkt</th>
|
||||||
|
<th>Volym (L)</th>
|
||||||
|
<th>Belopp</th>
|
||||||
|
<th>Snittpris/L</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in customer.summary.products %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ product.name }}</td>
|
||||||
|
<td>{{ product.volume }}</td>
|
||||||
|
<td>{{ product.amount }} Kr</td>
|
||||||
|
<td>{{ product.avg_price }} Kr</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="grand-total-row">
|
||||||
|
<td>Totalt</td>
|
||||||
|
<td>{{ customer.summary.total_volume }}</td>
|
||||||
|
<td>{{ customer.summary.grand_total }} Kr</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for card in customer.cards %}
|
{% for card in customer.cards %}
|
||||||
<div class="card-section">
|
<div class="card-section">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -154,7 +218,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="grand-total">
|
<div class="grand-total">
|
||||||
Totalsumma:<span>{{ customer.grand_total }} Kr</span>
|
Totalsumma:<span>{{ customer.summary.grand_total }} Kr</span>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user