import { useState, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import { ChevronDown, Search } from 'lucide-react'
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 (
Πληρωμή Καρτέλας
{tab.customer_name}
Υπόλοιπο καρτέλας
{fmt(tab.balance)}
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' }} />
{[['cash', 'Μετρητά'], ['card', 'Κάρτα'], ['other', 'Άλλο']].map(([v, l]) => (
))}
setNotes(e.target.value)} placeholder="προαιρετικό"
style={{ width: '100%', padding: '8px 11px', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 13, outline: 'none', boxSizing: 'border-box' }} />
)
}
// ── Forgive confirm ───────────────────────────────────────────────────────────
function ForgiveModal({ tab, onClose, onForgive, isPending }) {
const [reason, setReason] = useState('')
return (
Χάρισμα Καρτέλας
{tab.customer_name} — υπόλοιπο {fmt(tab.balance)}
)
}
// ── Tab detail card (collapsible) ─────────────────────────────────────────────
function TabDetail({ tab, onPay, onClose, onForgive, defaultExpanded = false }) {
const [expanded, setExpanded] = useState(defaultExpanded)
const sc = STATUS_CONFIG[tab.status] || STATUS_CONFIG.open
const canClose = tab.status === 'open' && tab.balance <= 0.005 && tab.entries.length > 0
return (
{/* Clickable header — always visible */}
{/* Expanded body */}
{expanded && (
<>
{/* Stats */}
{[
{ 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 => (
))}
{/* Entries */}
{tab.entries.length > 0 && (
Χρεώσεις
{tab.entries.map(e => (
{e.description || `Χρέωση #${e.id}`}
{fmt(e.amount)}
))}
)}
{/* Payments */}
{tab.payments.length > 0 && (
Πληρωμές
{tab.payments.map(p => (
{p.received_by_name} · {fmtDateTime(p.created_at)}
{p.payment_method && ({p.payment_method})}
{p.notes && — {p.notes}}
{fmt(p.amount)}
))}
)}
{/* Actions */}
{tab.status === 'open' && (
{tab.balance > 0.005 && (
)}
{canClose && (
)}
)}
>
)}
)
}
// ── Filter chip ───────────────────────────────────────────────────────────────
function FilterChip({ label, active, onClick }) {
return (
)
}
// ── Main page ─────────────────────────────────────────────────────────────────
export default function TabsPage() {
const qc = useQueryClient()
const [statusFilter, setStatusFilter] = useState('open') // 'open' | 'closed' | 'all'
const [search, setSearch] = useState('')
const [modal, setModal] = useState(null)
// Fetch open and closed separately — merge for client-side filtering
const { data: openTabs = [], isLoading: loadingOpen } = useQuery({
queryKey: ['tabs-open'],
queryFn: () => client.get('/api/tabs/').then(r => r.data),
staleTime: 15_000,
})
const { data: closedTabs = [], isLoading: loadingClosed } = useQuery({
queryKey: ['tabs-closed'],
queryFn: () => client.get('/api/tabs/', { params: { tab_status: 'closed' } }).then(r => r.data),
staleTime: 15_000,
})
const tabs = [...openTabs, ...closedTabs]
const isLoading = loadingOpen || loadingClosed
const payTab = useMutation({
mutationFn: ({ id, amount, payment_method, notes }) =>
client.post(`/api/tabs/${id}/pay`, { amount, payment_method, notes }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tabs-open'] }); qc.invalidateQueries({ queryKey: ['tabs-closed'] })
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-open'] }); qc.invalidateQueries({ queryKey: ['tabs-closed'] })
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-open'] }); qc.invalidateQueries({ queryKey: ['tabs-closed'] })
setModal(null)
toast.success('Το υπόλοιπο χαρίστηκε')
},
onError: () => toast.error('Σφάλμα'),
})
// Live client-side filtering
const filtered = useMemo(() => {
let result = tabs
if (statusFilter !== 'all') {
result = result.filter(t => {
if (statusFilter === 'open') return t.status === 'open'
if (statusFilter === 'closed') return t.status === 'closed' || t.status === 'forgiven'
return true
})
}
if (search.trim()) {
const q = search.trim().toLowerCase()
result = result.filter(t => t.customer_name?.toLowerCase().includes(q))
}
return result
}, [tabs, statusFilter, search])
const openCount = tabs.filter(t => t.status === 'open').length
const closedCount = tabs.filter(t => t.status === 'closed' || t.status === 'forgiven').length
const totalOutstanding = tabs.filter(t => t.status === 'open').reduce((s, t) => s + t.balance, 0)
return (
{/* Toolbar */}
{/* Search */}
setSearch(e.target.value)}
placeholder="Αναζήτηση πελάτη…"
className="pl-8 pr-3 py-1.5 text-[13px] bg-slate-50 border border-slate-200 rounded-lg w-48 focus:outline-none focus:ring-2 focus:ring-sky-500/30 focus:border-sky-400 transition placeholder:text-slate-400"
/>
{/* Status filter chips */}
setStatusFilter('open')} />
setStatusFilter('closed')} />
setStatusFilter('all')} />
{/* Summary */}
{statusFilter === 'open' && totalOutstanding > 0 && (
Σύνολο: {fmt(totalOutstanding)}
)}
{/* Tab list */}
{isLoading && (
Φόρτωση…
)}
{!isLoading && filtered.length === 0 && (
{search ? 'Δεν βρέθηκαν αποτελέσματα.' : 'Δεν υπάρχουν καρτέλες.'}
)}
{filtered.map(tab => (
setModal({ type: 'pay', tab: t })}
onClose={t => closeTab.mutate(t.id)}
onForgive={t => setModal({ type: 'forgive', tab: t })}
/>
))}
{modal?.type === 'pay' && (
setModal(null)}
isPending={payTab.isPending}
onPay={(amount, payment_method, notes) => payTab.mutate({ id: modal.tab.id, amount, payment_method, notes })}
/>
)}
{modal?.type === 'forgive' && (
setModal(null)}
isPending={forgiveTab.isPending}
onForgive={(reason) => forgiveTab.mutate({ id: modal.tab.id, reason })}
/>
)}
)
}