feat: added synopsis print on on print reports tab
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Printer, AlertCircle, Trophy } from 'lucide-react'
|
import { Printer, AlertCircle, Hash, Euro, FileText, X, ChevronDown, Loader2 } from 'lucide-react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
import client from '../../../api/client'
|
import client from '../../../api/client'
|
||||||
import { FilterBar, FilterSelect, FilterDateInput, WorkDayDateToggle } from '../shared/FilterBar'
|
import { FilterBar, FilterSelect, FilterDateInput, WorkDayDateToggle } from '../shared/FilterBar'
|
||||||
import { Panel, DataTable, THead, TH, TR, TD, StatusBadge } from '../shared/TablePrimitives'
|
import { Panel, DataTable, THead, TH, TR, TD, StatusBadge } from '../shared/TablePrimitives'
|
||||||
@@ -8,17 +9,263 @@ import StatCard from '../shared/StatCard'
|
|||||||
import EmptyState from '../shared/EmptyState'
|
import EmptyState from '../shared/EmptyState'
|
||||||
import SkeletonTable from '../shared/SkeletonTable'
|
import SkeletonTable from '../shared/SkeletonTable'
|
||||||
import ExportButton from '../shared/ExportButton'
|
import ExportButton from '../shared/ExportButton'
|
||||||
import { fmtNum, fmtDate, fmtTime, fmtDateTime } from '../shared/reportDesignTokens'
|
import { fmtNum, fmtEUR, fmtDate, fmtTime, fmtDateTime } from '../shared/reportDesignTokens'
|
||||||
|
|
||||||
function today() { return new Date().toISOString().slice(0, 10) }
|
function today() { return new Date().toISOString().slice(0, 10) }
|
||||||
function monthAgo() { const d = new Date(); d.setDate(d.getDate() - 30); return d.toISOString().slice(0, 10) }
|
function monthAgo() { const d = new Date(); d.setDate(d.getDate() - 30); return d.toISOString().slice(0, 10) }
|
||||||
|
|
||||||
|
// ─── Print Summary Modal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PRINT_TYPES = [
|
||||||
|
{
|
||||||
|
id: 'quick',
|
||||||
|
label: 'Γρήγορη Σύνοψη',
|
||||||
|
desc: 'Συγκεντρωτικά στατιστικά — λίγο χαρτί',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orders',
|
||||||
|
label: 'Όλες οι Παραγγελίες',
|
||||||
|
desc: 'Κάθε παραγγελία με σύνολο, χωρίς ανάλυση ειδών',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'detailed',
|
||||||
|
label: 'Πλήρης Ανάλυση',
|
||||||
|
desc: 'Κάθε παραγγελία + όλα τα είδη — καταναλώνει πολύ χαρτί',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function PrintSummaryModal({ onClose, logs, stats, printers, printerF, queryParams }) {
|
||||||
|
const [printType, setPrintType] = useState('quick')
|
||||||
|
const [targetPrinter, setTargetPrinter] = useState('browser')
|
||||||
|
const [printing, setPrinting] = useState(false)
|
||||||
|
|
||||||
|
const printerOptions = [
|
||||||
|
{ value: 'browser', label: 'Εκτύπωση μέσω browser' },
|
||||||
|
...printers.map(p => ({ value: String(p.id), label: p.name })),
|
||||||
|
]
|
||||||
|
|
||||||
|
async function handlePrint() {
|
||||||
|
if (targetPrinter !== 'browser') {
|
||||||
|
await handleThermalPrint()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleBrowserPrint()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleThermalPrint() {
|
||||||
|
// Map frontend print types to backend modes
|
||||||
|
// quick/orders → simple, detailed → extensive
|
||||||
|
const mode = printType === 'detailed' ? 'extensive' : 'simple'
|
||||||
|
const printerTargetId = printerF === 'all' ? 0 : parseInt(printerF, 10)
|
||||||
|
|
||||||
|
const fromDt = queryParams.from || (new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10) + 'T00:00:00')
|
||||||
|
const toDt = queryParams.to || (new Date().toISOString().slice(0, 10) + 'T23:59:59')
|
||||||
|
|
||||||
|
setPrinting(true)
|
||||||
|
try {
|
||||||
|
await client.post('/api/reports/print/printer', {
|
||||||
|
printer_target_id: printerTargetId,
|
||||||
|
printer_id: parseInt(targetPrinter, 10),
|
||||||
|
mode,
|
||||||
|
from_dt: fromDt,
|
||||||
|
to_dt: toDt,
|
||||||
|
})
|
||||||
|
toast.success('Η εκτύπωση στάλθηκε στον εκτυπωτή')
|
||||||
|
onClose()
|
||||||
|
} catch {
|
||||||
|
toast.error('Αποτυχία αποστολής στον εκτυπωτή')
|
||||||
|
} finally {
|
||||||
|
setPrinting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBrowserPrint() {
|
||||||
|
const win = window.open('', '_blank', 'width=800,height=700')
|
||||||
|
if (!win) { onClose(); return }
|
||||||
|
|
||||||
|
const styles = `
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Courier New', monospace; font-size: 12px; color: #000; padding: 24px; }
|
||||||
|
h1 { font-size: 16px; font-weight: bold; border-bottom: 2px solid #000; padding-bottom: 6px; margin-bottom: 12px; }
|
||||||
|
h2 { font-size: 13px; font-weight: bold; margin: 14px 0 6px; }
|
||||||
|
.row { display: flex; justify-content: space-between; padding: 3px 0; border-bottom: 1px dotted #ccc; }
|
||||||
|
.row:last-child { border-bottom: none; }
|
||||||
|
.section { margin-bottom: 16px; }
|
||||||
|
.order-block { border: 1px solid #ccc; border-radius: 4px; padding: 8px; margin-bottom: 8px; }
|
||||||
|
.order-header { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 4px; }
|
||||||
|
.item-row { padding-left: 12px; display: flex; justify-content: space-between; font-size: 11px; color: #333; }
|
||||||
|
.failed { color: #c00; }
|
||||||
|
.top-items li { padding: 2px 0; }
|
||||||
|
@media print { body { padding: 0; } }
|
||||||
|
</style>
|
||||||
|
`
|
||||||
|
|
||||||
|
const filterLabel = printerF === 'all'
|
||||||
|
? 'Όλοι οι Εκτυπωτές'
|
||||||
|
: (printers.find(p => String(p.id) === printerF)?.name || printerF)
|
||||||
|
|
||||||
|
let body = ''
|
||||||
|
|
||||||
|
if (printType === 'quick') {
|
||||||
|
const topItems = Object.entries(stats.itemCounts).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
||||||
|
const topWaiters = Object.entries(stats.waiterCounts).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
||||||
|
const avgItems = stats.total > 0 ? (stats.totalItemQty / stats.total).toFixed(1) : '—'
|
||||||
|
|
||||||
|
body = `
|
||||||
|
<h1>Σύνοψη Εκτυπωτή</h1>
|
||||||
|
<div class="section">
|
||||||
|
<div class="row"><span>Εκτυπωτής</span><span>${filterLabel}</span></div>
|
||||||
|
<div class="row"><span>Περίοδος</span><span>${stats.periodLabel}</span></div>
|
||||||
|
</div>
|
||||||
|
<h2>Στατιστικά</h2>
|
||||||
|
<div class="section">
|
||||||
|
<div class="row"><span>Συνολικές Εκτυπώσεις</span><span>${fmtNum(stats.total)}</span></div>
|
||||||
|
<div class="row"><span>Αποτυχημένες Εκτυπώσεις</span><span class="${stats.failed > 0 ? 'failed' : ''}">${fmtNum(stats.failed)}</span></div>
|
||||||
|
${stats.totalAmount != null ? `<div class="row"><span>Συνολικό Ποσό Παραγγελιών</span><span>${fmtEUR(stats.totalAmount)}</span></div>` : ''}
|
||||||
|
<div class="row"><span>Μέσος Αριθμός Ειδών ανά Παραγγελία</span><span>${avgItems}</span></div>
|
||||||
|
</div>
|
||||||
|
<h2>Top 3 Πιο Εκτυπωμένα Είδη</h2>
|
||||||
|
<div class="section">
|
||||||
|
<ol class="top-items" style="padding-left:16px">
|
||||||
|
${topItems.length ? topItems.map(([name, qty], i) => `<li>${i + 1}. ${name} — ${qty} τεμ.</li>`).join('') : '<li>—</li>'}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
${topWaiters.length ? `
|
||||||
|
<h2>Εκτυπώσεις ανά Σερβιτόρο</h2>
|
||||||
|
<div class="section">
|
||||||
|
${topWaiters.map(([name, cnt]) => `<div class="row"><span>${name}</span><span>${cnt}</span></div>`).join('')}
|
||||||
|
</div>` : ''}
|
||||||
|
`
|
||||||
|
} else if (printType === 'orders') {
|
||||||
|
body = `
|
||||||
|
<h1>Λίστα Παραγγελιών</h1>
|
||||||
|
<div class="section">
|
||||||
|
<div class="row"><span>Εκτυπωτής</span><span>${filterLabel}</span></div>
|
||||||
|
<div class="row"><span>Σύνολο Εγγραφών</span><span>${logs.length}</span></div>
|
||||||
|
</div>
|
||||||
|
<h2>Παραγγελίες</h2>
|
||||||
|
${logs.map(j => `
|
||||||
|
<div class="order-block ${!j.success ? 'failed' : ''}">
|
||||||
|
<div class="order-header">
|
||||||
|
<span>#${j.order_id} · ${j.table || '—'}</span>
|
||||||
|
<span>${fmtDateTime(j.printed_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row"><span>Εκτυπωτής</span><span>${j.printer_name}</span></div>
|
||||||
|
<div class="row"><span>Αποτέλεσμα</span><span>${j.success ? '✓ Επιτυχία' : '✗ Αποτυχία'}</span></div>
|
||||||
|
${j.order_total != null ? `<div class="row"><span>Σύνολο</span><span>${fmtEUR(j.order_total)}</span></div>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
body = `
|
||||||
|
<h1>Πλήρης Ανάλυση Εκτυπώσεων</h1>
|
||||||
|
<div class="section">
|
||||||
|
<div class="row"><span>Εκτυπωτής</span><span>${filterLabel}</span></div>
|
||||||
|
<div class="row"><span>Σύνολο Εγγραφών</span><span>${logs.length}</span></div>
|
||||||
|
</div>
|
||||||
|
${logs.map(j => `
|
||||||
|
<div class="order-block ${!j.success ? 'failed' : ''}">
|
||||||
|
<div class="order-header">
|
||||||
|
<span>#${j.order_id} · ${j.table || '—'}</span>
|
||||||
|
<span>${fmtDateTime(j.printed_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row"><span>Εκτυπωτής</span><span>${j.printer_name}</span></div>
|
||||||
|
<div class="row"><span>Αποτέλεσμα</span><span>${j.success ? '✓ Επιτυχία' : '✗ Αποτυχία'}</span></div>
|
||||||
|
${(j.items || []).map(i => `<div class="item-row"><span>${i.name}</span><span>×${i.quantity}</span></div>`).join('')}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
win.document.write(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Ιστορικό Εκτυπωτή</title>${styles}</head><body>${body}</body></html>`)
|
||||||
|
win.document.close()
|
||||||
|
win.focus()
|
||||||
|
win.print()
|
||||||
|
onClose()
|
||||||
|
} // end handleBrowserPrint
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
||||||
|
onMouseDown={e => { if (e.target === e.currentTarget) onClose() }}
|
||||||
|
>
|
||||||
|
<div className="relative w-full max-w-md rounded-xl border border-slate-200 bg-white shadow-xl">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-100 px-5 py-4">
|
||||||
|
<h2 className="text-[15px] font-semibold text-slate-900">Εκτύπωση Σύνοψης</h2>
|
||||||
|
<button onClick={onClose} className="rounded-md p-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 py-4 space-y-4 text-[13px] text-slate-700">
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-[11px] font-medium uppercase tracking-[0.08em] text-slate-500">Τύπος Εκτύπωσης</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{PRINT_TYPES.map(pt => (
|
||||||
|
<label key={pt.id} className={`flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition ${printType === pt.id ? 'border-sky-400 bg-sky-50' : 'border-slate-200 hover:border-slate-300 hover:bg-slate-50'}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="printType"
|
||||||
|
value={pt.id}
|
||||||
|
checked={printType === pt.id}
|
||||||
|
onChange={() => setPrintType(pt.id)}
|
||||||
|
className="mt-0.5 accent-sky-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-slate-800">{pt.label}</div>
|
||||||
|
<div className="text-[12px] text-slate-500">{pt.desc}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-[11px] font-medium uppercase tracking-[0.08em] text-slate-500">Εκτυπωτής Προορισμού</div>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={targetPrinter}
|
||||||
|
onChange={e => setTargetPrinter(e.target.value)}
|
||||||
|
className="w-full appearance-none rounded-md border border-slate-200 bg-white py-2 pl-3 pr-8 text-[13px] text-slate-700 shadow-[0_1px_0_rgba(15,23,42,0.04)] focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100"
|
||||||
|
>
|
||||||
|
{printerOptions.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="pointer-events-none absolute right-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 border-t border-slate-100 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md border border-slate-200 bg-white px-3.5 py-2 text-[13px] font-medium text-slate-700 shadow-[0_1px_0_rgba(15,23,42,0.04)] transition hover:bg-slate-50 hover:border-slate-300"
|
||||||
|
>
|
||||||
|
Ακύρωση
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handlePrint}
|
||||||
|
disabled={printing}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-md bg-sky-500 px-3.5 py-2 text-[13px] font-medium text-white shadow-[0_1px_0_rgba(15,23,42,0.08)] transition hover:bg-sky-600 active:bg-sky-700 disabled:opacity-60 disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
{printing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FileText className="h-3.5 w-3.5" />}
|
||||||
|
{printing ? 'Αποστολή...' : 'Εκτύπωση'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function PrinterHistory() {
|
export default function PrinterHistory() {
|
||||||
const [mode, setMode] = useState('range')
|
const [mode, setMode] = useState('range')
|
||||||
const [from, setFrom] = useState(monthAgo())
|
const [from, setFrom] = useState(monthAgo())
|
||||||
const [to, setTo] = useState(today())
|
const [to, setTo] = useState(today())
|
||||||
const [businessDayId, setBusinessDayId] = useState('all')
|
const [businessDayId, setBusinessDayId] = useState('all')
|
||||||
const [printerF, setPrinterF] = useState('all')
|
const [printerF, setPrinterF] = useState('all')
|
||||||
|
const [showPrintModal, setShowPrintModal] = useState(false)
|
||||||
|
|
||||||
const { data: printersData } = useQuery({ queryKey: ['meta-printers'], queryFn: () => client.get('/api/reports/meta/printers').then(r => r.data), staleTime: 5 * 60 * 1000 })
|
const { data: printersData } = useQuery({ queryKey: ['meta-printers'], queryFn: () => client.get('/api/reports/meta/printers').then(r => r.data), staleTime: 5 * 60 * 1000 })
|
||||||
const { data: bdData } = useQuery({ queryKey: ['business-days-list'], queryFn: () => client.get('/api/reports/business-days').then(r => r.data), staleTime: 60 * 1000 })
|
const { data: bdData } = useQuery({ queryKey: ['business-days-list'], queryFn: () => client.get('/api/reports/business-days').then(r => r.data), staleTime: 60 * 1000 })
|
||||||
@@ -35,17 +282,35 @@ export default function PrinterHistory() {
|
|||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const printerOptions = [{ value: 'all', label: 'Όλοι οι Εκτυπωτές' }, ...((printersData?.printers || []).map(p => ({ value: String(p.id), label: p.name })))]
|
const printers = printersData?.printers || []
|
||||||
|
const printerOptions = [{ value: 'all', label: 'Όλοι οι Εκτυπωτές' }, ...printers.map(p => ({ value: String(p.id), label: p.name }))]
|
||||||
const bdOptions = [{ value: 'all', label: 'Όλες οι Εργάσιμες Μέρες' }, ...((bdData?.business_days || []).map(bd => ({ value: String(bd.id), label: `${fmtDate(bd.opened_at)} · ${fmtTime(bd.opened_at)}` })))]
|
const bdOptions = [{ value: 'all', label: 'Όλες οι Εργάσιμες Μέρες' }, ...((bdData?.business_days || []).map(bd => ({ value: String(bd.id), label: `${fmtDate(bd.opened_at)} · ${fmtTime(bd.opened_at)}` })))]
|
||||||
|
|
||||||
const logs = data?.logs || []
|
const logs = data?.logs || []
|
||||||
const total = data?.total || 0
|
const total = data?.total || 0
|
||||||
const failed = data?.failed || 0
|
const failed = data?.failed || 0
|
||||||
|
|
||||||
// Find most printed item
|
// Derived stats for StatCards + Print modal
|
||||||
const itemCounts = {}
|
const itemCounts = {}
|
||||||
logs.forEach(l => (l.items || []).forEach(i => { itemCounts[i.name] = (itemCounts[i.name] || 0) + i.quantity }))
|
const waiterCounts = {}
|
||||||
|
let totalItemQty = 0
|
||||||
|
let totalAmount = null
|
||||||
|
|
||||||
|
logs.forEach(l => {
|
||||||
|
if (l.waiter) waiterCounts[l.waiter] = (waiterCounts[l.waiter] || 0) + 1
|
||||||
|
if (l.order_total != null) totalAmount = (totalAmount || 0) + l.order_total
|
||||||
|
;(l.items || []).forEach(i => {
|
||||||
|
itemCounts[i.name] = (itemCounts[i.name] || 0) + i.quantity
|
||||||
|
totalItemQty += i.quantity
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const topItem = Object.entries(itemCounts).sort((a, b) => b[1] - a[1])[0]
|
const topItem = Object.entries(itemCounts).sort((a, b) => b[1] - a[1])[0]
|
||||||
|
const periodLabel = mode === 'workday'
|
||||||
|
? (bdOptions.find(o => o.value === businessDayId)?.label || 'Εργάσιμη Μέρα')
|
||||||
|
: `${from} → ${to}`
|
||||||
|
|
||||||
|
const stats = { total, failed, totalAmount, itemCounts, waiterCounts, totalItemQty, periodLabel, topItem }
|
||||||
|
|
||||||
if (isLoading) return <div className="flex-1 overflow-y-auto p-6"><SkeletonTable rows={10} columns={7} /></div>
|
if (isLoading) return <div className="flex-1 overflow-y-auto p-6"><SkeletonTable rows={10} columns={7} /></div>
|
||||||
if (isError) return (
|
if (isError) return (
|
||||||
@@ -58,7 +323,16 @@ export default function PrinterHistory() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 min-h-0">
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
<FilterBar right={
|
<FilterBar right={
|
||||||
<ExportButton endpoint="/api/reports/printers/export" params={queryParams} filename={`printer-history-${from}-to-${to}.csv`} />
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPrintModal(true)}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-600 shadow-[0_1px_0_rgba(15,23,42,0.04)] transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900"
|
||||||
|
>
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
Εκτύπωση Σύνοψης
|
||||||
|
</button>
|
||||||
|
<ExportButton endpoint="/api/reports/printers/export" params={queryParams} filename={`printer-history-${from}-to-${to}.csv`} />
|
||||||
|
</div>
|
||||||
}>
|
}>
|
||||||
<WorkDayDateToggle mode={mode} onChange={setMode} />
|
<WorkDayDateToggle mode={mode} onChange={setMode} />
|
||||||
{mode === 'workday' ? (
|
{mode === 'workday' ? (
|
||||||
@@ -73,10 +347,17 @@ export default function PrinterHistory() {
|
|||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
<div className="grid grid-cols-4 gap-4 mb-4">
|
||||||
<StatCard label="Συνολικές Εκτυπώσεις" value={fmtNum(total)} icon={Printer} />
|
<StatCard label="Συνολικές Εκτυπώσεις" value={fmtNum(total)} icon={Printer} />
|
||||||
<StatCard label="Αποτυχημένες" value={fmtNum(failed)} sub={failed > 0 ? 'ελέγξτε τον εκτυπωτή' : 'όλα καλά'} icon={AlertCircle} />
|
<StatCard label="Αποτυχημένες" value={fmtNum(failed)} sub={failed > 0 ? 'ελέγξτε τον εκτυπωτή' : 'όλα καλά'} icon={AlertCircle} />
|
||||||
<StatCard label="Πιο Εκτυπωμένο" value={topItem ? topItem[0] : '—'} sub={topItem ? `${topItem[1]} τεμ.` : ''} icon={Trophy} accent />
|
<StatCard label="Παραγγελίες Εκτυπώθηκαν" value={fmtNum(total - failed)} sub="επιτυχείς εκτυπώσεις" icon={Hash} />
|
||||||
|
<StatCard
|
||||||
|
label="Συνολικό Ποσό Παραγγελιών"
|
||||||
|
value={totalAmount != null ? fmtEUR(totalAmount) : '—'}
|
||||||
|
sub={totalAmount == null ? 'μη διαθέσιμο' : undefined}
|
||||||
|
icon={Euro}
|
||||||
|
accent={totalAmount != null}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Panel title="Εργασίες Εκτύπωσης" padded={false}>
|
<Panel title="Εργασίες Εκτύπωσης" padded={false}>
|
||||||
@@ -115,6 +396,17 @@ export default function PrinterHistory() {
|
|||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showPrintModal && (
|
||||||
|
<PrintSummaryModal
|
||||||
|
onClose={() => setShowPrintModal(false)}
|
||||||
|
logs={logs}
|
||||||
|
stats={stats}
|
||||||
|
printers={printers}
|
||||||
|
printerF={printerF}
|
||||||
|
queryParams={queryParams}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user