feat: reports cost tracking, staff hourly rate, product form refactor

- Backend reports: fix cost/profit calculation to handle null unit_cost
  with has_gap flag; expose total_cost in product & workday summary
- StaffTab: add hourly_rate field in payroll section (admin-only)
- ProductFormModal: major refactor of form layout and structure
- ProductsTab: minor tweaks aligned with form changes
- Report pages (Today, WorkDaySummary, CategoryPerformance,
  ProductPerformance, RevenueTrends): UI improvements and cost data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 09:58:55 +03:00
parent 34b58b3f2e
commit 5da378a0ae
10 changed files with 673 additions and 330 deletions

View File

@@ -263,7 +263,7 @@ const inputStyle = {
fontSize: 13, outline: 'none', color: '#111827', background: '#fff', boxSizing: 'border-box',
}
const EMPTY_FORM = { username: '', full_name: '', nickname: '', mobile_phone: '', email: '', note: '', role: 'waiter', pin: '' }
const EMPTY_FORM = { username: '', full_name: '', nickname: '', mobile_phone: '', email: '', note: '', role: 'waiter', pin: '', hourly_rate: '' }
// ── Main page ─────────────────────────────────────────────────────────────────
export default function WaitersPage() {
@@ -424,6 +424,7 @@ export default function WaitersPage() {
nickname: newForm.nickname || null, mobile_phone: newForm.mobile_phone || null,
email: newForm.email || null, note: newForm.note || null,
pin: newForm.pin, role: newForm.role, is_active: true,
hourly_rate: newForm.hourly_rate !== '' ? parseFloat(newForm.hourly_rate) : null,
})}
/>
)}
@@ -584,8 +585,30 @@ function AddWaiterModal({ form, setForm, avatarFile, avatarPreview, setAvatarFil
</div>
</div>
{/* Column 3: PIN */}
<div style={{ padding: '20px 22px', display: 'flex', flexDirection: 'column', gap: 12, background: '#fafafa' }}>
{/* Column 3: Payroll + PIN */}
<div style={{ padding: '20px 22px', display: 'flex', flexDirection: 'column', gap: 16, background: '#fafafa' }}>
<div>
<SectionLabel>Μισθοδοσία</SectionLabel>
<FormField label="Ωριαία αμοιβή (€/ώρα)" desc="Μόνο για διαχειριστές. Δεν εμφανίζεται στον εργαζόμενο.">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="number" step="0.50" min="0"
style={{ ...inputStyle, width: 110 }}
placeholder="π.χ. 5.50"
value={form.hourly_rate ?? ''}
onChange={e => f('hourly_rate', e.target.value)}
/>
{form.hourly_rate && parseFloat(form.hourly_rate) > 0 && (
<span style={{ fontSize: 11.5, color: '#16a34a', fontWeight: 500 }}>
{parseFloat(form.hourly_rate).toFixed(2)}/ώρα
</span>
)}
{!form.hourly_rate && (
<span style={{ fontSize: 11, color: '#9ca3af' }}>Δεν παρακολουθείται</span>
)}
</div>
</FormField>
</div>
<SectionLabel>Κωδικός PIN</SectionLabel>
<p style={{ margin: 0, fontSize: 12, color: '#6b7280', lineHeight: 1.5 }}>
Ο 4ψήφιος κωδικός που θα χρησιμοποιεί ο εργαζόμενος για να ξεκλειδώσει την εφαρμογή. Μπορεί να αλλάξει οποτεδήποτε.
@@ -725,11 +748,14 @@ function EditWaiterModal({ waiter, form, setForm, avatarInputRef, isPending, isU
<div>
<SectionLabel>Πληροφορίες</SectionLabel>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<InfoBlock label="Ημερομηνία δημιουργίας"
value={waiter.created_at ? new Date(waiter.created_at).toLocaleDateString('el-GR') : null} />
<InfoBlock label="Κατάσταση"
value={waiter.is_active ? 'Ενεργός' : 'Αποκλεισμένος'}
valueColor={waiter.is_active ? '#16a34a' : '#ef4444'} />
{/* Row 1: Date + Status side by side */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<InfoBlock label="Εγγραφή"
value={waiter.created_at ? new Date(waiter.created_at).toLocaleDateString('el-GR') : null} />
<InfoBlock label="Κατάσταση"
value={waiter.is_active ? 'Ενεργός' : 'Αποκλεισμένος'}
valueColor={waiter.is_active ? '#16a34a' : '#ef4444'} />
</div>
<InfoBlock label="Ρόλος"
value={waiter.role === 'waiter' ? 'Σερβιτόρος' : 'Διαχειριστής'} />
{waiter.role === 'waiter' && (