feat: add payroll + tables served to shift detail and workday staff views

- business_day summary: waiter entries now include hourly_rate_snapshot, duration_hours,
  shift_pay (cumulative across day's shifts), tables_served (unique table count)
- ShiftDetailModal: 4-KPI row replaced with 2×4 grid —
  Row 1: Διάρκεια | Μισθός/Ώρα | Πληρωμή Βάρδιας | Αρχικά Μετρητά
  Row 2: Σύνολο Παραγγελιών | Εισπράξεις | Προς Παράδοση | Εξ. Τραπεζιών
- WorkdaySummaryModal WaitersTab: collapsed row uses ΠΑΡΑΓΓΕΛΙΕΣ | ΕΞ.ΤΡΑΠΕΖΙΩΝ |
  ΑΝΤΙΚ.ΕΙΣΠΡ. | ΕΙΣΠΡΑΞΗ; expanded grid is 2 rows:
  Row 1: ΕΝΑΡΞΗ | ΛΗΞΗ | ΜΙΣΘΟΣ/ΩΡΑ | ΜΙΣΘΟΣ (εως τώρα)
  Row 2: ΑΡΧΙΚΑ ΜΕΤΡΗΤΑ | ΤΑΜΕΙΟ ΤΩΡΑ | ΑΝΤΙΚ.ΕΙΣΠΡ. | ΕΞ.ΤΡΑΠΕΖΙΩΝ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:24:01 +03:00
parent e5b8ef32be
commit b15fb27a37
3 changed files with 108 additions and 42 deletions

View File

@@ -177,20 +177,42 @@ export default function ShiftDetailModal({ shiftId, shiftWaiterId, onClose }) {
{summary && (
<>
{/* KPI row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10, padding: '14px 24px', borderBottom: '1px solid #edeff1', flexShrink: 0 }}>
{[
{ label: 'Διάρκεια', value: fmtMins(summary.duration_minutes) },
{ label: 'Αρχικά μετρητά', value: summary.starting_cash != null ? fmtEUR(summary.starting_cash) : '—' },
{ label: 'Είσπραξη', value: fmtEUR(summary.total_collected), accent: '#2f9e5e' },
{ label: 'Προς παράδοση', value: fmtEUR(summary.net_to_deliver), accent: '#3758c9' },
].map(k => (
<div key={k.label} style={{ background: '#f9fafb', borderRadius: 10, padding: '10px 12px', textAlign: 'center' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#8a9099', textTransform: 'uppercase', letterSpacing: 0.5 }}>{k.label}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: k.accent || '#111315', fontFamily: 'ui-monospace,monospace', marginTop: 4 }}>{k.value}</div>
{/* KPI grid — 2 rows × 4 cols */}
{(() => {
const totalOrders = summary.orders?.length ?? 0
const tableSet = new Set(summary.orders?.map(o => o.table_name).filter(Boolean))
const tablesServed = tableSet.size
const durationHours = summary.duration_hours ?? (summary.duration_minutes != null ? summary.duration_minutes / 60 : null)
const durationLabel = durationHours != null
? (() => { const h = Math.floor(durationHours); const m = Math.round((durationHours - h) * 60); return h > 0 ? `${h}ω ${m}λ` : `${m}λ` })()
: fmtMins(summary.duration_minutes)
const row1 = [
{ label: 'Διάρκεια Βάρδιας', value: durationLabel },
{ label: 'Μισθός / Ώρα', value: summary.hourly_rate_snapshot != null ? fmtEUR(summary.hourly_rate_snapshot) : '—' },
{ label: 'Πληρωμή Βάρδιας', value: summary.shift_pay != null ? fmtEUR(summary.shift_pay) : 'Δεν παρακολουθείται', accent: summary.shift_pay != null ? '#059669' : undefined },
{ label: 'Αρχικά Μετρητά', value: summary.starting_cash != null ? fmtEUR(summary.starting_cash) : '—' },
]
const row2 = [
{ label: 'Σύνολο Παραγγελιών', value: String(totalOrders) },
{ label: 'Εισπράξεις', value: fmtEUR(summary.total_collected), accent: '#2f9e5e' },
{ label: 'Προς Παράδοση', value: fmtEUR(summary.net_to_deliver), accent: '#3758c9' },
{ label: 'Εξ. Τραπεζιών', value: String(tablesServed) },
]
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: '14px 24px', borderBottom: '1px solid #edeff1', flexShrink: 0 }}>
{[row1, row2].map((row, ri) => (
<div key={ri} style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 6 }}>
{row.map(k => (
<div key={k.label} style={{ background: '#f9fafb', borderRadius: 10, padding: '10px 12px', textAlign: 'center' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#8a9099', textTransform: 'uppercase', letterSpacing: 0.5 }}>{k.label}</div>
<div style={{ fontSize: 16, fontWeight: 700, color: k.accent || '#111315', fontFamily: 'ui-monospace,monospace', marginTop: 4 }}>{k.value}</div>
</div>
))}
</div>
))}
</div>
))}
</div>
)
})()}
{/* Colour legend */}
<div style={{ display: 'flex', gap: 12, padding: '8px 24px', background: '#fafafa', borderBottom: '1px solid #edeff1', flexShrink: 0, flexWrap: 'wrap' }}>