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:
@@ -170,25 +170,8 @@ function OverviewTab({ data }) {
|
||||
function WaitersTab({ data }) {
|
||||
const [expanded, setExpanded] = useState(null)
|
||||
|
||||
// Compute the widest rendered string for each of the 4 metric columns so we
|
||||
// can give every row the same fixed width — no DOM measurement needed, just
|
||||
// pick the longest formatted value across all waiters and use ch units.
|
||||
const colWidths = data.waiters.reduce(
|
||||
(acc, w) => ({
|
||||
orders: Math.max(acc.orders, String(w.orders_opened).length),
|
||||
items: Math.max(acc.items, String(w.items_ordered).length),
|
||||
value: Math.max(acc.value, fmt(w.total_items_value).length),
|
||||
collected: Math.max(acc.collected, fmt(w.total_collected).length),
|
||||
}),
|
||||
{ orders: 4, items: 4, value: 8, collected: 8 }
|
||||
)
|
||||
// Convert char-count to px using ~8.5px per char at font-size 16, plus label + padding
|
||||
const colPx = {
|
||||
orders: Math.max(colWidths.orders * 9 + 16, 72),
|
||||
items: Math.max(colWidths.items * 9 + 16, 72),
|
||||
value: Math.max(colWidths.value * 8 + 16, 96),
|
||||
collected: Math.max(colWidths.collected * 8 + 16, 96),
|
||||
}
|
||||
// Fixed column widths for the collapsed summary row
|
||||
const colPx = { orders: 88, tables: 88, items: 80, collected: 104 }
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
@@ -198,6 +181,10 @@ function WaitersTab({ data }) {
|
||||
{data.waiters.map(w => {
|
||||
const badge = roleBadge(w.role)
|
||||
const isOpen = expanded === w.waiter_id
|
||||
|
||||
// Compute current cash in drawer: starting_cash + collected so far
|
||||
const drawerNow = (w.starting_cash ?? 0) + (w.total_collected ?? 0)
|
||||
|
||||
return (
|
||||
<div key={w.waiter_id} style={{
|
||||
border: '1px solid #e5e7eb', borderRadius: 12, overflow: 'hidden',
|
||||
@@ -228,17 +215,16 @@ function WaitersTab({ data }) {
|
||||
}}>{badge.label}</span>
|
||||
</div>
|
||||
|
||||
{/* 4 fixed-width metric columns — same width for every row */}
|
||||
{/* 4 fixed-width metric columns */}
|
||||
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
|
||||
{[
|
||||
{ label: 'ΠΑΡΑΓΓΕΛΙΕΣ', value: w.orders_opened, width: colPx.orders, color: '#111315' },
|
||||
{ label: 'ΑΝΤΙΚΕΙΜ.', value: w.items_ordered, width: colPx.items, color: '#111315' },
|
||||
{ label: 'ΑΞΙΑ ΠΑΡ.', value: fmt(w.total_items_value),width: colPx.value, color: '#374151' },
|
||||
{ label: 'ΕΙΣΠΡΑΞΗ', value: fmt(w.total_collected), width: colPx.collected, color: '#16a34a' },
|
||||
{ label: 'ΠΑΡΑΓΓΕΛΙΕΣ', value: w.orders_opened, width: colPx.orders, color: '#111315' },
|
||||
{ label: 'ΕΞ. ΤΡΑΠΕΖΙΩΝ', value: w.tables_served ?? 0, width: colPx.tables, color: '#111315' },
|
||||
{ label: 'ΑΝΤΙΚ. ΕΙΣΠΡ.', value: w.items_paid, width: colPx.items, color: '#111315' },
|
||||
{ label: 'ΕΙΣΠΡΑΞΗ', value: fmt(w.total_collected), width: colPx.collected, color: '#16a34a' },
|
||||
].map(col => (
|
||||
<div key={col.label} style={{
|
||||
width: col.width, flexShrink: 0, textAlign: 'right',
|
||||
padding: '0 6px',
|
||||
width: col.width, flexShrink: 0, textAlign: 'right', padding: '0 6px',
|
||||
}}>
|
||||
<div style={{ fontSize: 10, color: '#9ca3af', fontWeight: 600, whiteSpace: 'nowrap' }}>{col.label}</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: col.color, whiteSpace: 'nowrap' }}>{col.value}</div>
|
||||
@@ -255,22 +241,56 @@ function WaitersTab({ data }) {
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div style={{ borderTop: '1px solid #f0f0ef', background: '#fafafa', padding: '14px 18px', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{/* 4 cells, full-width grid, equal columns */}
|
||||
<div style={{ borderTop: '1px solid #f0f0ef', background: '#fafafa', padding: '14px 18px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{/* Row 1: shift timing + payroll */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||
{[
|
||||
{ label: 'ΑΝΤΙΚ. ΕΙΣΠΡ.', value: w.items_paid, color: '#111315' },
|
||||
{ label: 'ΕΝΑΡΞΗ ΒΑΡΔΙΑΣ', value: fmtTime(w.shift_started_at), color: '#111315' },
|
||||
{
|
||||
label: 'ΛΗΞΗ ΒΑΡΔΙΑΣ',
|
||||
value: w.shift_ended_at ? fmtTime(w.shift_ended_at) : 'Βάρδια Ενεργή',
|
||||
color: w.shift_ended_at ? '#111315' : '#16a34a',
|
||||
},
|
||||
{
|
||||
label: 'ΜΙΣΘΟΣ / ΩΡΑ',
|
||||
value: w.hourly_rate_snapshot != null ? fmt(w.hourly_rate_snapshot) : '—',
|
||||
color: '#111315',
|
||||
},
|
||||
{
|
||||
label: 'ΜΙΣΘΟΣ (εως τώρα)',
|
||||
value: w.shift_pay != null ? fmt(w.shift_pay) : 'Δεν παρακολ.',
|
||||
color: w.shift_pay != null ? '#059669' : '#9ca3af',
|
||||
},
|
||||
].map(cell => (
|
||||
<div key={cell.label} style={{ background: 'white', border: '1px solid #e5e7eb', borderRadius: 8, padding: '9px 12px' }}>
|
||||
<div style={{ fontSize: 10, color: '#9ca3af', fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{cell.label}</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: cell.color, marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{cell.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Row 2: cash + collections */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||
{[
|
||||
{
|
||||
label: 'ΑΡΧΙΚΑ ΜΕΤΡΗΤΑ',
|
||||
value: w.starting_cash != null ? fmt(w.starting_cash) : '—',
|
||||
color: '#111315',
|
||||
},
|
||||
{
|
||||
label: 'ΤΑΜΕΙΟ ΤΩΡΑ',
|
||||
value: fmt(drawerNow),
|
||||
color: '#3758c9',
|
||||
},
|
||||
{
|
||||
label: 'ΑΝΤΙΚ. ΕΙΣΠΡ.',
|
||||
value: w.items_paid,
|
||||
color: '#111315',
|
||||
},
|
||||
{
|
||||
label: 'ΕΞ. ΤΡΑΠΕΖΙΩΝ',
|
||||
value: w.tables_served ?? 0,
|
||||
color: '#111315',
|
||||
},
|
||||
].map(cell => (
|
||||
<div key={cell.label} style={{ background: 'white', border: '1px solid #e5e7eb', borderRadius: 8, padding: '9px 12px' }}>
|
||||
<div style={{ fontSize: 10, color: '#9ca3af', fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{cell.label}</div>
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
Reference in New Issue
Block a user