Skip to content

Commit 2b31cd9

Browse files
committed
fixed time when sending to fira, added report by prodajnaMjesta, added gotovina and kartica fields to izvjestaj po artiklima
1 parent 4ac2333 commit 2b31cd9

3 files changed

Lines changed: 115 additions & 17 deletions

File tree

backend/fira.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,20 @@ export async function handleOrderFiscalization(order, options = {}) {
6363
}
6464

6565

66-
// Format datetime for FIRA API (LocalDateTime format without timezone)
67-
const d = new Date(order.createdAt);
68-
const pad = (n) => String(n).padStart(2, '0');
69-
const createdAt = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
66+
// Format datetime for FIRA API (LocalDateTime format in Europe/Zagreb timezone)
67+
// Uses formatToParts keyed by type to avoid locale separator quirks and
68+
// hourCycle 'h23' to prevent the midnight="24:00:00" ICU bug.
69+
const _dtf = new Intl.DateTimeFormat('en-CA', {
70+
timeZone: 'Europe/Zagreb',
71+
year: 'numeric',
72+
month: '2-digit', day: '2-digit',
73+
hour: '2-digit', minute: '2-digit', second: '2-digit',
74+
hourCycle: 'h23',
75+
});
76+
const _parts = Object.fromEntries(
77+
_dtf.formatToParts(new Date(order.createdAt)).map(({ type, value }) => [type, value])
78+
);
79+
const createdAt = `${_parts.year}-${_parts.month}-${_parts.day}T${_parts.hour}:${_parts.minute}:${_parts.second}`;
7080

7181
// Billing address for FIRA API
7282
// Include email if available - FIRA will send email invoice if configured

frontend/src/pages/admin/ProdajnaMjesta.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export default function ProdajnaMjesta() {
213213
setFormData({ ...formData, firaApiKey: e.target.checked ? "" : originalKey });
214214
}}
215215
/>
216-
Promjeni ključ
216+
Promijeni ključ
217217
</label>
218218
{changingKey && (
219219
<input
@@ -293,7 +293,7 @@ export default function ProdajnaMjesta() {
293293
<td>{loc.name}</td>
294294
<td><code>{loc.businessSpace}</code></td>
295295
<td><code>{loc.paymentDevice}</code></td>
296-
<td><code>{loc.firaApiKey || "-"}</code></td>
296+
<td><code>{loc.firaApiKey ? `****${loc.firaApiKey.slice(-4)}` : "-"}</code></td>
297297
<td className="actions">
298298
<button onClick={() => handleEdit(loc)} className="icon-btn edit" title="Uredi">
299299
<i className="fas fa-edit"></i>

frontend/src/pages/admin/povijest.jsx

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,32 +77,41 @@ export default function Povijest() {
7777

7878
const getArticlesReport = () => {
7979
const articleMap = {};
80-
80+
8181
filteredTransactions.forEach(t => {
8282
if (t.receipt?.items && t.receipt.status !== 'STORNO' && t.receipt.status !== 'RACUN_STORNIRAN') {
83+
const isCash = t.receipt.paymentType === 'GOTOVINA';
8384
t.receipt.items.forEach(item => {
8485
const name = item.article?.name || "N/A";
8586
if (!articleMap[name]) {
8687
articleMap[name] = {
8788
name,
8889
price: item.price,
8990
quantity: 0,
91+
cashTotal: 0,
92+
cardTotal: 0,
9093
total: 0
9194
};
9295
}
9396
const qty = parseFloat(item.quantity);
97+
const itemTotal = qty * Math.abs(parseFloat(item.price));
9498
articleMap[name].quantity += qty;
95-
articleMap[name].total += qty * Math.abs(parseFloat(item.price));
99+
articleMap[name].total += itemTotal;
100+
if (isCash) {
101+
articleMap[name].cashTotal += itemTotal;
102+
} else {
103+
articleMap[name].cardTotal += itemTotal;
104+
}
96105
});
97106
}
98107
});
99-
108+
100109
return Object.values(articleMap).sort((a, b) => b.total - a.total);
101110
};
102111

103112
const getPaymentReport = () => {
104113
const paymentMap = {};
105-
114+
106115
filteredTransactions.forEach(t => {
107116
if (t.receipt?.paymentType && t.receipt.status !== 'STORNO' && t.receipt.status !== 'RACUN_STORNIRAN') {
108117
const method = t.receipt.paymentType;
@@ -113,16 +122,33 @@ export default function Povijest() {
113122
paymentMap[method].count += 1;
114123
}
115124
});
116-
125+
117126
return Object.values(paymentMap).sort((a, b) => b.total - a.total);
118127
};
119128

129+
const getProdajnaMjestaReport = () => {
130+
const map = {};
131+
filteredTransactions.forEach(t => {
132+
if (t.receipt?.status !== 'STORNO' && t.receipt?.status !== 'RACUN_STORNIRAN') {
133+
const name = t.receipt?.prodajnoMjestoNaziv || "Nepoznato";
134+
if (!map[name]) map[name] = { name, total: 0, count: 0, gotovina: 0, kartica: 0 };
135+
const amt = parseFloat(t.amount);
136+
map[name].total += amt;
137+
map[name].count += 1;
138+
const pt = t.receipt?.paymentType;
139+
if (pt === 'GOTOVINA') map[name].gotovina += amt;
140+
else if (pt === 'KARTICA') map[name].kartica += amt;
141+
}
142+
});
143+
return Object.values(map).sort((a, b) => b.total - a.total);
144+
};
145+
120146
const exportTransactionsToExcel = () => {
121147
const excelData = filteredTransactions.map(t => ({
122148
"Broj Računa": t.receipt?.invoiceNumber || "N/A",
123149
"Iznos (€)": parseFloat(t.amount).toFixed(2),
124150
"Plaćanje": t.receipt?.paymentType || "N/A",
125-
"Status": t.receipt?.status === 'STORNO' ? 'Storno' : t.receipt?.status === 'RACUN_STORNIRAN' ? 'Otkazano' : 'Gotovo',
151+
"Status": t.receipt?.status === 'STORNO' ? 'Storno' : t.receipt?.status === 'RACUN_STORNIRAN' ? 'Otkazano' : 'Aktivan',
126152
"Prodavač": t.user?.name || "Nepoznato",
127153
"Datum": new Date(t.createdAt).toLocaleDateString("hr-HR"),
128154
"Vrijeme": new Date(t.createdAt).toLocaleTimeString("hr-HR", { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
@@ -141,6 +167,8 @@ export default function Povijest() {
141167
"Naziv Artikla": a.name,
142168
"Cijena (€)": parseFloat(a.price).toFixed(2),
143169
"Količina": a.quantity,
170+
"Gotovina (€)": a.cashTotal.toFixed(2),
171+
"Kartica (€)": a.cardTotal.toFixed(2),
144172
"Suma (€)": a.total.toFixed(2)
145173
}));
146174

@@ -154,7 +182,7 @@ export default function Povijest() {
154182
const paymentReport = getPaymentReport();
155183
const excelData = paymentReport.map(p => ({
156184
"Način Plaćanja": p.method,
157-
"Broj Računa": p.count,
185+
"Količina": p.count,
158186
"Suma (€)": p.total.toFixed(2)
159187
}));
160188

@@ -164,6 +192,21 @@ export default function Povijest() {
164192
XLSX.writeFile(workbook, `KSET_Izvjestaj_Placanje_${startDate}_${endDate}.xlsx`);
165193
};
166194

195+
const exportProdajnaMjestaToExcel = () => {
196+
const report = getProdajnaMjestaReport();
197+
const excelData = report.map(p => ({
198+
"Prodajno Mjesto": p.name,
199+
"Količina": p.count,
200+
"Gotovina (€)": p.gotovina.toFixed(2),
201+
"Kartica (€)": p.kartica.toFixed(2),
202+
"Suma (€)": p.total.toFixed(2)
203+
}));
204+
const worksheet = XLSX.utils.json_to_sheet(excelData);
205+
const workbook = XLSX.utils.book_new();
206+
XLSX.utils.book_append_sheet(workbook, worksheet, "Prodaja po Prodajnim Mjestima");
207+
XLSX.writeFile(workbook, `KSET_Izvjestaj_ProdajnaMjesta_${startDate}_${endDate}.xlsx`);
208+
};
209+
167210
const handleExport = () => {
168211
switch(reportType) {
169212
case 'transactions':
@@ -175,6 +218,9 @@ export default function Povijest() {
175218
case 'payment':
176219
exportPaymentToExcel();
177220
break;
221+
case 'prodajnaMjesta':
222+
exportProdajnaMjestaToExcel();
223+
break;
178224
default:
179225
break;
180226
}
@@ -190,7 +236,9 @@ export default function Povijest() {
190236

191237
const articlesReport = getArticlesReport();
192238
const paymentReport = getPaymentReport();
193-
const totalAmount = filteredTransactions.reduce((sum, t) => sum + parseFloat(t.amount), 0);
239+
const totalAmount = filteredTransactions
240+
.filter(t => t.receipt?.status !== 'STORNO' && t.receipt?.status !== 'RACUN_STORNIRAN')
241+
.reduce((sum, t) => sum + parseFloat(t.amount), 0);
194242

195243
if (loading) return <div style={{ padding: '40px' }}>Učitavanje...</div>;
196244

@@ -223,6 +271,7 @@ export default function Povijest() {
223271
<option value="transactions">Povijest transakcija</option>
224272
<option value="articles">Prodaja po artiklima</option>
225273
<option value="payment">Prodaja po načinu plaćanja</option>
274+
<option value="prodajnaMjesta">Prodaja po prodajnim mjestima</option>
226275
</select>
227276
</div>
228277

@@ -490,6 +539,8 @@ export default function Povijest() {
490539
<th style={thStyle}>Naziv Artikla</th>
491540
<th style={thStyle}>Cijena</th>
492541
<th style={thStyle}>Količina</th>
542+
<th style={thStyle}>Gotovina</th>
543+
<th style={thStyle}>Kartica</th>
493544
<th style={thStyle}>Suma</th>
494545
</tr>
495546
</thead>
@@ -499,11 +550,13 @@ export default function Povijest() {
499550
<td style={tdStyle}>{article.name}</td>
500551
<td style={tdStyle}>{parseFloat(article.price).toFixed(2)}</td>
501552
<td style={tdStyle}>{article.quantity}</td>
553+
<td style={tdStyle}>{article.cashTotal.toFixed(2)}</td>
554+
<td style={tdStyle}>{article.cardTotal.toFixed(2)}</td>
502555
<td style={{ ...tdStyle, fontWeight: '600' }}>{article.total.toFixed(2)}</td>
503556
</tr>
504557
))}
505558
<tr style={{ backgroundColor: '#edf2f7', fontWeight: 'bold' }}>
506-
<td colSpan="3" style={{ ...tdStyle, textAlign: 'right' }}>UKUPNO:</td>
559+
<td colSpan="5" style={{ ...tdStyle, textAlign: 'right' }}>UKUPNO:</td>
507560
<td style={{ ...tdStyle, fontWeight: 'bold' }}>{articlesReport.reduce((sum, a) => sum + a.total, 0).toFixed(2)}</td>
508561
</tr>
509562
</tbody>
@@ -515,7 +568,7 @@ export default function Povijest() {
515568
<thead>
516569
<tr style={{ backgroundColor: '#edf2f7', textAlign: 'left' }}>
517570
<th style={thStyle}>Način Plaćanja</th>
518-
<th style={thStyle}>Broj Računa</th>
571+
<th style={thStyle}>Količina</th>
519572
<th style={thStyle}>Suma</th>
520573
</tr>
521574
</thead>
@@ -535,6 +588,41 @@ export default function Povijest() {
535588
</tbody>
536589
</table>
537590
)}
591+
592+
{reportType === 'prodajnaMjesta' && (() => {
593+
const report = getProdajnaMjestaReport();
594+
return (
595+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
596+
<thead>
597+
<tr style={{ backgroundColor: '#edf2f7', textAlign: 'left' }}>
598+
<th style={thStyle}>Prodajno Mjesto</th>
599+
<th style={thStyle}>Količina</th>
600+
<th style={thStyle}>Gotovina</th>
601+
<th style={thStyle}>Kartica</th>
602+
<th style={thStyle}>Suma</th>
603+
</tr>
604+
</thead>
605+
<tbody>
606+
{report.map((pm, idx) => (
607+
<tr key={idx} style={{ borderBottom: '1px solid #edf2f7' }}>
608+
<td style={tdStyle}>{pm.name}</td>
609+
<td style={tdStyle}>{pm.count}</td>
610+
<td style={tdStyle}>{pm.gotovina.toFixed(2)}</td>
611+
<td style={tdStyle}>{pm.kartica.toFixed(2)}</td>
612+
<td style={{ ...tdStyle, fontWeight: '600' }}>{pm.total.toFixed(2)}</td>
613+
</tr>
614+
))}
615+
<tr style={{ backgroundColor: '#edf2f7', fontWeight: 'bold' }}>
616+
<td colSpan="1" style={{ ...tdStyle, textAlign: 'right' }}>UKUPNO:</td>
617+
<td style={tdStyle}>{report.reduce((sum, p) => sum + p.count, 0)}</td>
618+
<td style={tdStyle}>{report.reduce((sum, p) => sum + p.gotovina, 0).toFixed(2)}</td>
619+
<td style={tdStyle}>{report.reduce((sum, p) => sum + p.kartica, 0).toFixed(2)}</td>
620+
<td style={{ ...tdStyle, fontWeight: 'bold' }}>{report.reduce((sum, p) => sum + p.total, 0).toFixed(2)}</td>
621+
</tr>
622+
</tbody>
623+
</table>
624+
);
625+
})()}
538626
</div>
539627
</div>
540628
);
@@ -546,7 +634,7 @@ function StatusBadge({ status }) {
546634
};
547635
if (status === 'STORNO') return <span style={{ ...styles, backgroundColor: '#fed7d7', color: '#9b2c2c' }}>Storno</span>;
548636
if (status === 'RACUN_STORNIRAN') return <span style={{ ...styles, backgroundColor: '#feebc8', color: '#9c4221' }}>Otkazano</span>;
549-
return <span style={{ ...styles, backgroundColor: '#c6f6d5', color: '#22543d' }}>Gotovo</span>;
637+
return <span style={{ ...styles, backgroundColor: '#c6f6d5', color: '#22543d' }}>Aktivan</span>;
550638
}
551639

552640
const thStyle = { padding: '15px', fontSize: '14px', color: '#4a5568', textTransform: 'uppercase', letterSpacing: '0.05em' };

0 commit comments

Comments
 (0)