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:
2026-06-08 03:07:22 +03:00
parent e9bdf61a4d
commit 2ff48730f7
10 changed files with 819 additions and 1 deletions

View File

@@ -15,6 +15,7 @@ import NotesPage from './pages/NotesPage'
import ContactsPage from './pages/ContactsPage'
import ExpensesPage from './pages/ExpensesPage'
import CustomersPage from './pages/CustomersPage'
import TabsPage from './pages/TabsPage'
import client from './api/client'
function Spinner() {
@@ -89,6 +90,7 @@ export default function App() {
<Route path="contacts" element={<ContactsPage />} />
<Route path="expenses" element={<ExpensesPage />} />
<Route path="customers" element={<CustomersPage />} />
<Route path="tabs" element={<TabsPage />} />
<Route path="online-orders" element={<OnlineOrdersPage />} />
<Route path="reports" element={<ReportsPage />} />
<Route path="settings" element={<SettingsPage />} />

View File

@@ -1,6 +1,6 @@
import { NavLink } from 'react-router-dom'
import { useState, useEffect, useRef } from 'react'
import { BarChart2, LayoutGrid, ClipboardList, Package, Settings, ChevronRight, ChevronLeft, ShoppingBag, NotebookPen, Receipt, BookUser, Users } from 'lucide-react'
import { BarChart2, LayoutGrid, ClipboardList, Package, Settings, ChevronRight, ChevronLeft, ShoppingBag, NotebookPen, Receipt, BookUser, Users, CreditCard } from 'lucide-react'
import { getIncomingOrders } from '../api/client'
export default function Sidebar() {
@@ -30,6 +30,7 @@ export default function Sidebar() {
{ to: '/expenses', icon: Receipt, label: 'Έξοδα' },
{ to: '/contacts', icon: BookUser, label: 'Επαφές' },
{ to: '/customers', icon: Users, label: 'Πελάτες' },
{ to: '/tabs', icon: CreditCard, label: 'Καρτέλες' },
{ to: '/settings', icon: Settings, label: 'Ρυθμίσεις' },
]

View File

@@ -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) ? (

View File

@@ -241,6 +241,12 @@ export default function OrderDetailPage({ orderId: propOrderId, readOnly = false
onError: () => toast.error('Σφάλμα πληρωμής'),
})
const tabItem = useMutation({
mutationFn: (itemId) => client.post(`/api/orders/${orderId}/items/${itemId}/tab`),
onSuccess: () => { toast.success('Αντικείμενο μεταφέρθηκε στην καρτέλα'); invalidate() },
onError: (e) => toast.error(e?.response?.data?.detail || 'Σφάλμα'),
})
function requestPay(item_ids) {
setPayPending(item_ids)
}
@@ -448,6 +454,14 @@ export default function OrderDetailPage({ orderId: propOrderId, readOnly = false
>
Πληρωμή
</button>
{order.customer_id && (
<button
onClick={() => tabItem.mutate(item.id)}
style={{ fontSize: 12, padding: '0 8px', height: 32, borderRadius: 6, border: '1px solid #bfdbfe', background: '#eff6ff', color: '#1d4ed8', cursor: 'pointer', fontWeight: 600, whiteSpace: 'nowrap' }}
>
📋 Καρτέλα
</button>
)}
<button
onClick={() => setConfirmAction({ type: 'cancelItem', payload: item.id })}
className="btn btn-danger text-xs px-2 py-1 min-h-0 h-8"
@@ -456,6 +470,11 @@ export default function OrderDetailPage({ orderId: propOrderId, readOnly = false
</button>
</>
)}
{item.status === 'tabbed' && (
<span style={{ fontSize: 11, fontWeight: 700, padding: '2px 8px', borderRadius: 99, background: '#eff6ff', color: '#1d4ed8', border: '1px solid #bfdbfe' }}>
📋 Καρτέλα
</span>
)}
</div>
</div>
)

View File

@@ -0,0 +1,342 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import client from '../api/client'
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmt(n) {
if (n == null) return '—'
return n.toLocaleString('el-GR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'
}
function fmtDateTime(iso) {
if (!iso) return '—'
return new Date(iso).toLocaleString('el-GR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
}
function daysSince(iso) {
if (!iso) return null
const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000)
if (days === 0) return 'Σήμερα'
if (days === 1) return '1 μέρα'
return `${days} μέρες`
}
const STATUS_CONFIG = {
open: { label: 'Ανοιχτή', bg: '#fee2e2', color: '#dc2626' },
closed: { label: 'Κλειστή', bg: '#dcfce7', color: '#16a34a' },
forgiven: { label: 'Χαρίστηκε', bg: '#f3f4f6', color: '#6b7280' },
}
// ── Pay modal ─────────────────────────────────────────────────────────────────
function PayModal({ tab, onClose, onPay, isPending }) {
const [amount, setAmount] = useState(String(Math.round(tab.balance * 100) / 100))
const [method, setMethod] = useState('cash')
const [notes, setNotes] = useState('')
const parsed = parseFloat(amount)
const canPay = parsed > 0 && parsed <= tab.balance + 0.005
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 60, padding: 20 }}>
<div style={{ background: 'white', borderRadius: 16, width: '100%', maxWidth: 400, boxShadow: '0 20px 60px rgba(0,0,0,0.18)' }}>
<div style={{ padding: '18px 22px 14px', borderBottom: '1px solid #f0f0ef' }}>
<div style={{ fontSize: 16, fontWeight: 700 }}>Πληρωμή Καρτέλας</div>
<div style={{ fontSize: 13, color: '#6b7280', marginTop: 2 }}>{tab.customer_name}</div>
</div>
<div style={{ padding: '18px 22px', display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ background: '#f9fafb', borderRadius: 10, padding: '12px 14px', display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 13, color: '#6b7280' }}>Υπόλοιπο καρτέλας</span>
<span style={{ fontSize: 15, fontWeight: 700, color: '#dc2626' }}>{fmt(tab.balance)}</span>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: '#374151', display: 'block', marginBottom: 5 }}>Ποσό πληρωμής ()</label>
<input type="number" step="0.01" min="0.01"
value={amount} onChange={e => setAmount(e.target.value)} autoFocus
style={{ width: '100%', padding: '9px 12px', border: '1.5px solid #e5e7eb', borderRadius: 8, fontSize: 16, fontWeight: 600, outline: 'none', boxSizing: 'border-box' }} />
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: '#374151', display: 'block', marginBottom: 5 }}>Τρόπος πληρωμής</label>
<div style={{ display: 'flex', gap: 6 }}>
{[['cash', 'Μετρητά'], ['card', 'Κάρτα'], ['other', 'Άλλο']].map(([v, l]) => (
<button key={v} onClick={() => setMethod(v)}
style={{
flex: 1, padding: '7px', borderRadius: 7, fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
border: `1.5px solid ${method === v ? '#111827' : '#e5e7eb'}`,
background: method === v ? '#111827' : 'white',
color: method === v ? 'white' : '#6b7280',
}}>{l}</button>
))}
</div>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: '#374151', display: 'block', marginBottom: 5 }}>Σημείωση</label>
<input value={notes} onChange={e => setNotes(e.target.value)} placeholder="προαιρετικό"
style={{ width: '100%', padding: '8px 11px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 13, outline: 'none', boxSizing: 'border-box' }} />
</div>
</div>
<div style={{ padding: '14px 22px', borderTop: '1px solid #f0f0ef', display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
<button onClick={onClose} style={{ padding: '8px 18px', border: '1px solid #e5e7eb', background: 'white', borderRadius: 8, fontSize: 13, cursor: 'pointer' }}>Ακύρωση</button>
<button onClick={() => onPay(parsed, method, notes || null)} disabled={!canPay || isPending}
style={{ padding: '8px 20px', border: 'none', borderRadius: 8, fontSize: 13, fontWeight: 600, cursor: canPay ? 'pointer' : 'default', background: canPay ? '#16a34a' : '#e5e7eb', color: canPay ? 'white' : '#9ca3af' }}>
{isPending ? 'Αποθήκευση…' : 'Καταγραφή Πληρωμής'}
</button>
</div>
</div>
</div>
)
}
// ── Forgive confirm ───────────────────────────────────────────────────────────
function ForgiveModal({ tab, onClose, onForgive, isPending }) {
const [reason, setReason] = useState('')
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 60, padding: 20 }}>
<div style={{ background: 'white', borderRadius: 16, width: '100%', maxWidth: 400, boxShadow: '0 20px 60px rgba(0,0,0,0.18)' }}>
<div style={{ padding: '18px 22px 14px', borderBottom: '1px solid #f0f0ef' }}>
<div style={{ fontSize: 16, fontWeight: 700, color: '#dc2626' }}>Χάρισμα Καρτέλας</div>
<div style={{ fontSize: 13, color: '#6b7280', marginTop: 2 }}>{tab.customer_name} υπόλοιπο {fmt(tab.balance)}</div>
</div>
<div style={{ padding: '18px 22px', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ padding: '12px 14px', background: '#fef2f2', borderRadius: 10, border: '1px solid #fecaca', fontSize: 13, color: '#7f1d1d', lineHeight: 1.5 }}>
Το υπόλοιπο των {fmt(tab.balance)} θα διαγραφεί. Η ενέργεια δεν αναιρείται.
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: '#374151', display: 'block', marginBottom: 5 }}>Αιτιολογία</label>
<input value={reason} onChange={e => setReason(e.target.value)} placeholder="π.χ. Καλή θέληση"
style={{ width: '100%', padding: '8px 11px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 13, outline: 'none', boxSizing: 'border-box' }} />
</div>
</div>
<div style={{ padding: '14px 22px', borderTop: '1px solid #f0f0ef', display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
<button onClick={onClose} style={{ padding: '8px 18px', border: '1px solid #e5e7eb', background: 'white', borderRadius: 8, fontSize: 13, cursor: 'pointer' }}>Ακύρωση</button>
<button onClick={() => onForgive(reason || null)} disabled={isPending}
style={{ padding: '8px 20px', border: 'none', borderRadius: 8, fontSize: 13, fontWeight: 600, cursor: 'pointer', background: '#dc2626', color: 'white' }}>
{isPending ? 'Αποθήκευση…' : 'Χάρισμα Υπολοίπου'}
</button>
</div>
</div>
</div>
)
}
// ── Tab detail card ───────────────────────────────────────────────────────────
function TabDetail({ tab, onPay, onClose, onForgive }) {
const sc = STATUS_CONFIG[tab.status] || STATUS_CONFIG.open
const canClose = tab.status === 'open' && tab.balance <= 0.005 && tab.entries.length > 0
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: 12, overflow: 'hidden', background: 'white' }}>
{/* Header */}
<div style={{ padding: '14px 18px', background: '#f9fafb', borderBottom: '1px solid #e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
<div>
<div style={{ fontSize: 15, fontWeight: 700, color: '#111315' }}>{tab.customer_name}</div>
<div style={{ fontSize: 12, color: '#9ca3af', marginTop: 1 }}>
Άνοιγμα: {fmtDateTime(tab.opened_at)} · {daysSince(tab.opened_at)} ανοιχτή
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 22, fontWeight: 800, color: tab.balance > 0 ? '#dc2626' : '#16a34a' }}>
{fmt(tab.balance)}
</span>
<span style={{ fontSize: 11.5, fontWeight: 700, padding: '2px 8px', borderRadius: 99, background: sc.bg, color: sc.color }}>{sc.label}</span>
</div>
</div>
{/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 1, background: '#f0f0ef' }}>
{[
{ label: 'Συνολικές χρεώσεις', value: fmt(tab.total_charged) },
{ label: 'Πληρωμένο', value: fmt(tab.total_paid), color: '#16a34a' },
{ label: 'Υπόλοιπο', value: fmt(tab.balance), color: tab.balance > 0 ? '#dc2626' : '#16a34a' },
].map(s => (
<div key={s.label} style={{ background: 'white', padding: '10px 14px', textAlign: 'center' }}>
<div style={{ fontSize: 10, color: '#9ca3af', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }}>{s.label}</div>
<div style={{ fontSize: 15, fontWeight: 800, color: s.color || '#111315', marginTop: 2 }}>{s.value}</div>
</div>
))}
</div>
{/* Entries */}
{tab.entries.length > 0 && (
<div style={{ padding: '12px 18px', borderBottom: '1px solid #f0f0ef' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 8 }}>Χρεώσεις</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{tab.entries.map(e => (
<div key={e.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 13 }}>
<span style={{ color: '#374151' }}>{e.description || `Χρέωση #${e.id}`}</span>
<span style={{ fontWeight: 600, color: '#dc2626', whiteSpace: 'nowrap', marginLeft: 12 }}>{fmt(e.amount)}</span>
</div>
))}
</div>
</div>
)}
{/* Payments */}
{tab.payments.length > 0 && (
<div style={{ padding: '12px 18px', borderBottom: '1px solid #f0f0ef' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 8 }}>Πληρωμές</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{tab.payments.map(p => (
<div key={p.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 13 }}>
<span style={{ color: '#374151' }}>
{p.received_by_name} · {fmtDateTime(p.created_at)}
{p.payment_method && <span style={{ color: '#9ca3af', marginLeft: 6 }}>({p.payment_method})</span>}
{p.notes && <span style={{ color: '#9ca3af', marginLeft: 6 }}> {p.notes}</span>}
</span>
<span style={{ fontWeight: 600, color: '#16a34a', whiteSpace: 'nowrap', marginLeft: 12 }}>{fmt(p.amount)}</span>
</div>
))}
</div>
</div>
)}
{/* Actions */}
{tab.status === 'open' && (
<div style={{ padding: '12px 18px', display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{tab.balance > 0.005 && (
<button onClick={() => onPay(tab)}
style={{ padding: '7px 16px', borderRadius: 8, border: 'none', background: '#16a34a', color: 'white', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
Πληρωμή
</button>
)}
{canClose && (
<button onClick={() => onClose(tab)}
style={{ padding: '7px 16px', borderRadius: 8, border: '1px solid #d1d5db', background: 'white', color: '#374151', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
Κλείσιμο Καρτέλας
</button>
)}
<button onClick={() => onForgive(tab)}
style={{ padding: '7px 16px', borderRadius: 8, border: '1px solid #fecaca', background: 'white', color: '#dc2626', fontSize: 13, fontWeight: 500, cursor: 'pointer', marginLeft: 'auto' }}>
Χάρισμα Υπολοίπου
</button>
</div>
)}
</div>
)
}
// ── Main page ─────────────────────────────────────────────────────────────────
export default function TabsPage() {
const qc = useQueryClient()
const [showAll, setShowAll] = useState(false)
const [modal, setModal] = useState(null) // { type: 'pay'|'forgive', tab }
const { data: tabs = [], isLoading } = useQuery({
queryKey: ['tabs', showAll],
queryFn: () => client.get('/api/tabs/', { params: showAll ? { tab_status: 'all' } : {} }).then(r => r.data),
staleTime: 15_000,
})
// Refetch tabs with all statuses when toggled
const { data: allTabs = [] } = useQuery({
queryKey: ['tabs-all'],
queryFn: () => client.get('/api/tabs/', { params: { tab_status: 'closed' } }).then(r => r.data),
staleTime: 30_000,
enabled: showAll,
})
const payTab = useMutation({
mutationFn: ({ id, amount, payment_method, notes }) =>
client.post(`/api/tabs/${id}/pay`, { amount, payment_method, notes }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tabs'] })
qc.invalidateQueries({ queryKey: ['tabs-all'] })
setModal(null)
toast.success('Πληρωμή καταγράφηκε')
},
onError: (e) => toast.error(e?.response?.data?.detail || 'Σφάλμα'),
})
const closeTab = useMutation({
mutationFn: (id) => client.post(`/api/tabs/${id}/close`, {}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tabs'] })
toast.success('Καρτέλα έκλεισε')
},
onError: (e) => toast.error(e?.response?.data?.detail || 'Σφάλμα'),
})
const forgiveTab = useMutation({
mutationFn: ({ id, reason }) => client.post(`/api/tabs/${id}/forgive`, { reason }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tabs'] })
setModal(null)
toast.success('Το υπόλοιπο χαρίστηκε')
},
onError: () => toast.error('Σφάλμα'),
})
const openTabs = tabs.filter(t => t.status === 'open')
const totalOutstanding = openTabs.reduce((s, t) => s + t.balance, 0)
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* Header */}
<div style={{ padding: '18px 28px 14px', borderBottom: '1px solid #f0f0ef', flexShrink: 0, background: 'white', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 800, color: '#111315' }}>Καρτέλες</h1>
<p style={{ margin: '2px 0 0', fontSize: 13, color: '#9ca3af' }}>
{openTabs.length} ανοιχτές καρτέλες · Σύνολο οφειλόμενων:{' '}
<strong style={{ color: totalOutstanding > 0 ? '#dc2626' : '#16a34a' }}>{fmt(totalOutstanding)}</strong>
</p>
</div>
<button
onClick={() => setShowAll(s => !s)}
style={{ padding: '7px 14px', borderRadius: 8, border: '1px solid #e5e7eb', background: 'white', fontSize: 12.5, cursor: 'pointer', color: '#6b7280' }}
>
{showAll ? 'Μόνο ανοιχτές' : 'Εμφάνιση κλειστών'}
</button>
</div>
{/* Tab list */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 28px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{isLoading && <div style={{ color: '#9ca3af', fontSize: 13, textAlign: 'center', padding: '48px 0' }}>Φόρτωση</div>}
{!isLoading && openTabs.length === 0 && (
<div style={{ textAlign: 'center', color: '#9ca3af', fontSize: 14, padding: '48px 0' }}>
Δεν υπάρχουν ανοιχτές καρτέλες.
</div>
)}
{openTabs.map(tab => (
<TabDetail
key={tab.id}
tab={tab}
onPay={t => setModal({ type: 'pay', tab: t })}
onClose={t => closeTab.mutate(t.id)}
onForgive={t => setModal({ type: 'forgive', tab: t })}
/>
))}
{showAll && allTabs.length > 0 && (
<>
<div style={{ fontSize: 11, fontWeight: 700, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: '0.05em', marginTop: 8 }}>Κλειστές / Χαρισμένες</div>
{allTabs.map(tab => (
<TabDetail key={tab.id} tab={tab} onPay={() => {}} onClose={() => {}} onForgive={() => {}} />
))}
</>
)}
</div>
{modal?.type === 'pay' && (
<PayModal
tab={modal.tab}
onClose={() => setModal(null)}
isPending={payTab.isPending}
onPay={(amount, payment_method, notes) => payTab.mutate({ id: modal.tab.id, amount, payment_method, notes })}
/>
)}
{modal?.type === 'forgive' && (
<ForgiveModal
tab={modal.tab}
onClose={() => setModal(null)}
isPending={forgiveTab.isPending}
onForgive={(reason) => forgiveTab.mutate({ id: modal.tab.id, reason })}
/>
)}
</div>
)
}