feat: Phase 2G — pay-later tab system
Backend:
- New models: Tab, TabEntry, TabPayment (3 new tables)
- Tab: open|closed|forgiven, one open tab per customer enforced
- TabEntry: what went ON the tab (per-item snapshot with description)
- TabPayment: what came OFF the tab; balance = sum(entries) - sum(payments), never stored
- New router /api/tabs/:
- GET / — list open tabs (sorted by balance desc); ?tab_status= override
- GET /{id} — full tab detail with entries + payments
- POST / — open new tab for a customer (400 if one already exists)
- POST /{id}/entries — add entry directly (amount + description)
- POST /{id}/pay — record payment; validates amount <= balance
- POST /{id}/close — close tab (requires balance = 0)
- POST /{id}/forgive — write off remaining balance
- GET /customer/{customer_id} — all tabs for a customer (open first)
- POST /api/orders/{id}/items/{item_id}/tab:
- Requires order has a customer assigned
- Finds or auto-creates open tab for that customer
- Sets order_item.status = "tabbed" (new valid status value)
- Creates TabEntry with auto-generated description
- Broadcasts order_updated SSE event
- Migrations: CREATE TABLE IF NOT EXISTS for tabs, tab_entries, tab_payments
Frontend:
- TabsPage (/tabs): open tabs list sorted by balance; each card shows charges/payments
history, live balance, Πληρωμή modal (defaults to full balance), Κλείσιμο button
(only shown when balance=0), Χάρισμα Υπολοίπου with confirm modal + reason
- CustomersPage CustomerDetail: open tab banner showing balance + entry count
(appears between stats and contact info when customer has an open tab)
- OrderDetailPage: 📋 Καρτέλα button appears on active items when order has a customer;
tabbed items show a 📋 Καρτέλα badge; tabItem mutation calls /items/{id}/tab
- Sidebar: CreditCard icon for Καρτέλες (after Πελάτες)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,13 @@ function CustomerDetail({ customer, onEdit, onClose }) {
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const { data: tabsData = [] } = useQuery({
|
||||
queryKey: ['customer-tabs', customer.id],
|
||||
queryFn: () => client.get(`/api/tabs/customer/${customer.id}`).then(r => r.data),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const openTab = tabsData.find(t => t.status === 'open')
|
||||
const orders = data?.orders || []
|
||||
|
||||
return (
|
||||
@@ -128,6 +135,21 @@ function CustomerDetail({ customer, onEdit, onClose }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Open tab banner */}
|
||||
{openTab && (
|
||||
<div style={{ padding: '10px 22px', borderBottom: '1px solid #f0f0ef', flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 10 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 700, color: '#dc2626' }}>Ανοιχτή Καρτέλα</div>
|
||||
<div style={{ fontSize: 12, color: '#9ca3af', marginTop: 1 }}>
|
||||
{openTab.entries.length} χρεώσεις · πληρωμένο {fmt(openTab.total_paid)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 800, color: '#dc2626' }}>{fmt(openTab.balance)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact info */}
|
||||
<div style={{ padding: '12px 22px', borderBottom: '1px solid #f0f0ef', flexShrink: 0 }}>
|
||||
{(customer.phone || customer.email || customer.notes) ? (
|
||||
|
||||
Reference in New Issue
Block a user