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:
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts'
|
||||
import { TrendingUp, ReceiptText, Clock, Users, Trophy, XCircle, Package, Sunset } from 'lucide-react'
|
||||
import { TrendingUp, ReceiptText, Clock, Users, Trophy, XCircle, Package, Sunset, PiggyBank, ShoppingBag } from 'lucide-react'
|
||||
import client from '../../../api/client'
|
||||
import { FilterBar } from '../shared/FilterBar'
|
||||
import { Panel, DataTable, THead, TH, TR, TD, StatusBadge, WaiterAvatar, ChartTooltip } from '../shared/TablePrimitives'
|
||||
@@ -94,6 +94,22 @@ export default function Today() {
|
||||
<span>·</span>
|
||||
<span><span className="font-mono font-semibold text-slate-900">{fmtEUR(bd.orders_closed ? bd.revenue / bd.orders_closed : 0)}</span> μέσος όρος</span>
|
||||
</div>
|
||||
{bd.trackable_profit > 0 && (
|
||||
<div className="mt-4 flex items-center gap-3 border-t border-sky-100 pt-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-[10px] font-medium uppercase tracking-[0.08em] text-slate-400">Μεικτό Κέρδος</div>
|
||||
<div className="font-mono text-[22px] font-semibold tabular-nums text-green-700">
|
||||
{fmtEUR(bd.trackable_profit)}
|
||||
</div>
|
||||
</div>
|
||||
{bd.revenue > 0 && (
|
||||
<div className="rounded-md bg-green-50 px-2.5 py-1 text-[13px] font-semibold text-green-700 ring-1 ring-inset ring-green-200">
|
||||
{((bd.trackable_profit / bd.revenue) * 100).toFixed(0)}% margin
|
||||
{bd.has_gap && <span className="ml-1 text-[10px] text-amber-500">⚠ μερικό</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-7 grid grid-cols-3 gap-4">
|
||||
@@ -104,6 +120,17 @@ export default function Today() {
|
||||
<StatCard label="Κορυφαίο Προϊόν" value={bd.top_product.name} sub={`${bd.top_product.qty} τεμ.`} icon={Trophy} />
|
||||
)}
|
||||
<StatCard label="Ακυρώσεις" value={fmtNum(bd.cancellations)} icon={XCircle} />
|
||||
{bd.total_cost > 0 && (
|
||||
<StatCard label="Κόστος Πωλήσεων" value={fmtEUR(bd.total_cost)} sub="κόστος ειδών" icon={ShoppingBag} />
|
||||
)}
|
||||
{bd.trackable_profit > 0 && (
|
||||
<StatCard
|
||||
label="Μεικτό Κέρδος"
|
||||
value={fmtEUR(bd.trackable_profit)}
|
||||
sub={bd.has_gap ? '⚠ μερικό κέρδος' : `${((bd.trackable_profit / bd.revenue) * 100).toFixed(0)}% margin`}
|
||||
icon={PiggyBank}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,11 +165,16 @@ export default function Today() {
|
||||
<DataTable>
|
||||
<THead>
|
||||
<TH>#</TH><TH>Τραπέζι</TH><TH>Άνοιξε</TH><TH>Έκλεισε</TH>
|
||||
<TH align="right">Είδη</TH><TH align="right">Σύνολο</TH><TH>Κατάσταση</TH>
|
||||
<TH align="right">Είδη</TH><TH align="right">Σύνολο</TH><TH align="right">Κέρδος</TH><TH>Κατάσταση</TH>
|
||||
</THead>
|
||||
<tbody>
|
||||
{orders.slice(0, 30).map(o => {
|
||||
const total = (o.items || []).filter(i => ['active', 'paid'].includes(i.status)).reduce((s, i) => s + i.unit_price * i.quantity, 0)
|
||||
const activeItems = (o.items || []).filter(i => ['active', 'paid'].includes(i.status))
|
||||
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
|
||||
const costedItems = activeItems.filter(i => i.unit_cost != null)
|
||||
const orderProfit = costedItems.length > 0
|
||||
? costedItems.reduce((s, i) => s + (i.unit_price - i.unit_cost) * i.quantity, 0)
|
||||
: null
|
||||
const isCancelled = o.status === 'cancelled'
|
||||
return (
|
||||
<TR key={o.id} striped className={isCancelled ? 'opacity-50' : ''}>
|
||||
@@ -152,6 +184,12 @@ export default function Today() {
|
||||
<TD mono>{fmtDateTime(o.closed_at)}</TD>
|
||||
<TD mono align="right">{(o.items || []).length}</TD>
|
||||
<TD mono align="right" className="font-semibold text-slate-900">{fmtEUR(total)}</TD>
|
||||
<TD mono align="right">
|
||||
{orderProfit != null
|
||||
? <span className={orderProfit >= 0 ? 'text-green-700' : 'text-red-600'}>{fmtEUR(orderProfit)}</span>
|
||||
: <span className="text-slate-300">—</span>
|
||||
}
|
||||
</TD>
|
||||
<TD><StatusBadge status={o.status} pulse /></TD>
|
||||
</TR>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user