+
+ {/* ── Header ── */}
+
+
+ {isNew ? 'Νέο προϊόν' : `Επεξεργασία — ${product.name}`}
+
+
-
setField('name', e.target.value)} autoFocus />
-
-
-
-
-
setField('base_price', e.target.value)} />
-
-
-
-
-
+ {/* ── Body: left/right split ── */}
+
- {/* Image */}
- {product.id && (
-
-
- {product.image_url &&

}
-
setImageFile(e.target.files[0])} />
-
- )}
+ {/* LEFT: product info */}
+
+
Στοιχεία προϊόντος
- {/* Options */}
-
- {form.options.map((opt, i) => (
- setOption(i, 'name', v)} onCost={v => setOption(i, 'extra_cost', v)}
- onRemove={() => removeOption(i)} costLabel="+/- €" />
- ))}
-
-
- {/* Ingredients */}
-
- {form.ingredients.map((ing, i) => (
- setIngredient(i, 'name', v)} onCost={v => setIngredient(i, 'extra_cost', v)}
- onRemove={() => removeIngredient(i)} costLabel="+/- €" />
- ))}
-
-
- {/* Preference Sets */}
-
- {form.preference_sets.map((ps, si) => (
-
-
- setPrefSetName(si, e.target.value)} />
-
-
- {ps.choices.map((ch, ci) => (
-
setChoice(si, ci, 'name', v)} onCost={v => setChoice(si, ci, 'extra_cost', v)}
- onRemove={() => removeChoice(si, ci)} costLabel="+/- €" indent />
- ))}
-
+
+
+ setField('name', e.target.value)} autoFocus placeholder="π.χ. Espresso" />
- ))}
-
-
-
-
+
+
+
setField('base_price', v)} className="w-full" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isNew && (
+
+
+
+ {product.image_url && (
+

+ )}
+
setImageFile(e.target.files[0])} />
+
+
+ )}
+
+
+ {/* RIGHT: tabs */}
+
+ {/* Tab bar */}
+
+ {tabs.map(tab => {
+ if (tab.isAdd) return (
+
+ )
+ const isActive = activeTab === tab.key
+ return (
+
+ )
+ })}
+
+
+ {/* Tab content */}
+
+
+ {/* ── Ingredients tab ── */}
+ {activeTab === 'ingredients' && (
+
+
+
Υλικά που ο πελάτης μπορεί να αφαιρέσει.
+
+
+ {!form.ingredients.length &&
Δεν υπάρχουν υλικά.
}
+
+ {form.ingredients.map((ing, i) => (
+
+ ))}
+
+
+ )}
+
+ {/* ── Options tab ── */}
+ {activeTab === 'options' && (
+
+
+
Προσθέτα (πολλαπλή επιλογή). Κάθε επιλογή μπορεί να έχει δικές της υπο-επιλογές.
+
+
+ {!form.options.length &&
Δεν υπάρχουν επιλογές.
}
+
+ {form.options.map((opt, i) => (
+
+
+
moveOptionSubChoice(i, sci, dir)}
+ onToggleDefault={sci => toggleOptionSubDefault(i, sci)}
+ onChange={(sci, k, v) => setOptionSubChoice(i, sci, k, v)}
+ onRemove={sci => removeOptionSubChoice(i, sci)}
+ onAdd={() => addOptionSubChoice(i)}
+ />
+
+ ))}
+
+
+ )}
+
+ {/* ── Preference set tabs ── */}
+ {typeof activeTab === 'number' && form.preference_sets[activeTab] && (() => {
+ const si = activeTab
+ const ps = form.preference_sets[si]
+ const hasSharedSubset = !!(ps.shared_subset)
+ return (
+
+
+ setPrefSetField(si, 'name', e.target.value)} autoFocus />
+
+
+
+
+ ● = προεπιλογή (κλικ ξανά για αποεπιλογή) · ⊘ = απενεργοποιεί το κοινό υπο-σύνολο
+
+
+
+ {ps.choices.map((ch, ci) => (
+
+
+
moveChoice(si, ci, -1)} onDown={() => moveChoice(si, ci, 1)}
+ disableUp={ci === 0} disableDown={ci === ps.choices.length - 1} />
+ toggleDefaultChoice(si, ci)}
+ />
+ setChoice(si, ci, 'name', e.target.value)} />
+ setChoice(si, ci, 'extra_cost', v)}
+ allowNegative className="w-32" />
+ {hasSharedSubset && (
+
+ )}
+
+
+
+
moveSubChoice(si, ci, sci, dir)}
+ onToggleDefault={sci => setSubChoice(si, ci, sci, 'is_default', !ch.sub_choices[sci]?.is_default)}
+ onChange={(sci, k, v) => setSubChoice(si, ci, sci, k, v)}
+ onRemove={sci => removeSubChoice(si, ci, sci)}
+ onAdd={() => addSubChoice(si, ci)}
+ />
+
+ ))}
+
+
+
+
+ {/* Shared subset */}
+
+
+
+
Κοινό υπο-σύνολο
+
Εμφανίζεται για όλες τις επιλογές εκτός αυτών με ⊘
+
+ {!ps.shared_subset ? (
+
+ ) : (
+
+ )}
+
+ {ps.shared_subset && (
+
+
+
+ setSharedSubsetName(si, e.target.value)} />
+
+
+ {(ps.shared_subset.choices || []).map((sc, sci) => (
+
+
moveSharedSubsetChoice(si, sci, -1)} onDown={() => moveSharedSubsetChoice(si, sci, 1)}
+ disableUp={sci === 0} disableDown={sci === ps.shared_subset.choices.length - 1} />
+ setSharedSubsetChoice(si, sci, 'is_default', !sc.is_default)} />
+ setSharedSubsetChoice(si, sci, 'name', e.target.value)} />
+ setSharedSubsetChoice(si, sci, 'extra_cost', v)}
+ allowNegative className="w-32 text-sm" />
+
+
+ ))}
+
+
+
+ )}
+
+
+ )
+ })()}
+
+
+
+
+ {/* ── Footer ── */}
+
+
+
+ {!isNew && (
+
+ )}
+
+
)
}
-
-function Section({ title, onAdd, addLabel, children }) {
- return (
-
-
-
-
-
- {children}
-
- )
-}
-
-function CostRow({ name, cost, onName, onCost, onRemove, costLabel, indent }) {
- return (
-
- onName(e.target.value)} />
- onCost(e.target.value)} />
-
-
- )
-}
diff --git a/manager_dashboard/src/pages/TablesPage.jsx b/manager_dashboard/src/pages/TablesPage.jsx
index b086b99..a96ed43 100644
--- a/manager_dashboard/src/pages/TablesPage.jsx
+++ b/manager_dashboard/src/pages/TablesPage.jsx
@@ -4,14 +4,40 @@ import toast from 'react-hot-toast'
import client from '../api/client'
import ConfirmModal from '../components/ConfirmModal'
+const ZONE_COLORS = ['#6366f1','#0ea5e9','#10b981','#f59e0b','#ef4444','#ec4899','#8b5cf6','#14b8a6','#f97316','#64748b']
+
+function ZoneColorPicker({ value, onChange }) {
+ return (
+
+
+ )
+}
+
export default function TablesPage() {
const qc = useQueryClient()
const [addModal, setAddModal] = useState(false)
const [editModal, setEditModal] = useState(null)
- const [batchModal, setBatchModal] = useState(null) // group id or null
+ const [batchModal, setBatchModal] = useState(null) // group object or null
const [groupModal, setGroupModal] = useState(null) // null | {} | group object
- const [confirmDelete, setConfirmDelete] = useState(null) // { id, hard }
+ const [confirmDelete, setConfirmDelete] = useState(null)
const [showInactive, setShowInactive] = useState(false)
+ const [activeTab, setActiveTab] = useState('all') // 'all' | group.id
const { data: tables = [], isLoading } = useQuery({
queryKey: ['tables-all', showInactive],
@@ -29,15 +55,6 @@ export default function TablesPage() {
}
const invalidateGroups = () => qc.invalidateQueries({ queryKey: ['table-groups'] })
- // Next auto-increment number within a group (or global)
- function nextNumber(groupId) {
- const relevant = groupId
- ? tables.filter(t => t.group_id === groupId)
- : tables
- if (relevant.length === 0) return 1
- return Math.max(...relevant.map(t => t.number)) + 1
- }
-
const createTable = useMutation({
mutationFn: (body) => client.post('/api/tables/', body),
onSuccess: () => { toast.success('Τραπέζι δημιουργήθηκε'); setAddModal(false); invalidate() },
@@ -70,81 +87,125 @@ export default function TablesPage() {
mutationFn: (body) => groupModal?.id
? client.put(`/api/tables/groups/${groupModal.id}`, body)
: client.post('/api/tables/groups', body),
- onSuccess: () => { toast.success('Γκρουπ αποθηκεύτηκε'); setGroupModal(null); invalidateGroups(); invalidate() },
+ onSuccess: () => { toast.success('Ζώνη αποθηκεύτηκε'); setGroupModal(null); invalidateGroups(); invalidate() },
onError: (err) => toast.error(err.response?.data?.detail || 'Σφάλμα'),
})
const deleteGroup = useMutation({
mutationFn: (id) => client.delete(`/api/tables/groups/${id}`),
- onSuccess: () => { toast.success('Γκρουπ διαγράφηκε'); setGroupModal(null); invalidateGroups(); invalidate() },
+ onSuccess: () => { toast.success('Ζώνη διαγράφηκε'); setGroupModal(null); invalidateGroups(); invalidate() },
onError: () => toast.error('Σφάλμα'),
})
- // Group tables by group
- const grouped = [
- { group: null, tables: tables.filter(t => !t.group_id) },
- ...groups.map(g => ({ group: g, tables: tables.filter(t => t.group_id === g.id) })),
- ].filter(section => section.tables.length > 0 || section.group)
+ // Filter tables for the active tab
+ const visibleTables = activeTab === 'all'
+ ? tables
+ : activeTab === 'ungrouped'
+ ? tables.filter(t => !t.group_id)
+ : tables.filter(t => t.group_id === activeTab)
if (isLoading) return
Φόρτωση…
return (
-
+
+ {/* Header */}
Τραπέζια
-
- {grouped.map(({ group, tables: gt }) => (
-
- {group && (
-
-
{group.name}
-
setGroupModal(group)} className="text-xs text-gray-400 hover:text-gray-600">✏️
-
setBatchModal(group.id)} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-7">+ Μαζική προσθήκη
+ {/* Zone tabs */}
+
+ {[
+ { id: 'all', label: 'Όλα', color: null },
+ ...groups.map(g => ({ id: g.id, label: g.prefix ? `${g.prefix} – ${g.name}` : g.name, color: g.color, group: g })),
+ ...(tables.some(t => !t.group_id) ? [{ id: 'ungrouped', label: 'Χωρίς ζώνη', color: null }] : []),
+ ].map(tab => (
+ setActiveTab(tab.id)}
+ className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${
+ activeTab === tab.id
+ ? 'bg-white border border-b-white border-gray-200 -mb-px text-primary-700'
+ : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
+ }`}
+ >
+ {tab.color && }
+ {tab.label}
+
+ ({tab.id === 'all' ? tables.length : tab.id === 'ungrouped' ? tables.filter(t => !t.group_id).length : tables.filter(t => t.group_id === tab.id).length})
+
+
+ ))}
+
+
+ {/* Zone header (when viewing a specific zone) */}
+ {activeTab !== 'all' && activeTab !== 'ungrouped' && (() => {
+ const g = groups.find(g => g.id === activeTab)
+ if (!g) return null
+ return (
+
+
+ {g.name}
+ {g.prefix && {g.prefix}}
- )}
- {!group && gt.length > 0 &&
Χωρίς γκρουπ
}
-
-
- {gt.length === 0 && (
-
Δεν υπάρχουν τραπέζια σε αυτό το γκρουπ.
- )}
- {gt.map(t => (
-
-
{t.number}
-
{t.label || '—'}
- {!t.is_active &&
Ανενεργό}
-
setEditModal(t)} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">Επεξεργασία
- {t.is_active
- ?
setConfirmDelete({ id: t.id, hard: false })} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9 text-amber-600 hover:bg-amber-50">Απενεργ.
- :
updateTable.mutate({ id: t.id, is_active: true })} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9 text-green-600 hover:bg-green-50">Ενεργοπ.
- }
-
setConfirmDelete({ id: t.id, hard: true })} className="btn btn-danger text-sm px-3 py-1.5 min-h-0 h-9">Διαγραφή
-
- ))}
+
setGroupModal(g)} className="text-xs text-gray-400 hover:text-gray-600 underline">Επεξεργασία ζώνης
+
setBatchModal(g)} className="btn btn-secondary text-xs px-3 py-1 min-h-0 h-7">+ Μαζική προσθήκη
-
- ))}
+ )
+ })()}
- {tables.length === 0 && (
-
Δεν υπάρχουν τραπέζια. Προσθέστε ένα.
- )}
+ {/* Tables list */}
+
+ {visibleTables.length === 0 && (
+
+ {showInactive ? 'Δεν υπάρχουν τραπέζια.' : 'Δεν υπάρχουν ενεργά τραπέζια.'}
+
+ )}
+ {visibleTables.map((t, idx) => (
+
+
{idx + 1}
+
{t.label || `Τραπέζι ${t.number}`}
+ {t.group && (
+
+ {t.group.prefix ? `${t.group.prefix}` : t.group.name}
+
+ )}
+ {!t.is_active &&
Ανενεργό}
+
setEditModal(t)} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-8">Επεξεργασία
+ {t.is_active
+ ?
!t.has_active_order && setConfirmDelete({ id: t.id, hard: false })}
+ disabled={t.has_active_order}
+ title={t.has_active_order ? 'Υπάρχει ενεργή παραγγελία' : undefined}
+ className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-8 text-amber-600 hover:bg-amber-50 disabled:opacity-40 disabled:cursor-not-allowed"
+ >Απενεργ.
+ :
updateTable.mutate({ id: t.id, is_active: true })} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-8 text-green-600 hover:bg-green-50">Ενεργοπ.
+ }
+
!t.has_active_order && setConfirmDelete({ id: t.id, hard: true })}
+ disabled={t.has_active_order}
+ title={t.has_active_order ? 'Υπάρχει ενεργή παραγγελία' : undefined}
+ className="btn btn-danger text-sm px-3 py-1.5 min-h-0 h-8 disabled:opacity-40 disabled:cursor-not-allowed"
+ >Διαγραφή
+
+ ))}
+
{/* Add single table */}
{addModal && (
createTable.mutate({ number: Number(f.number), label: f.label || null, group_id: f.group_id ? Number(f.group_id) : null })}
+ onSave={(f) => createTable.mutate({ label: f.label || null, group_id: f.group_id ? Number(f.group_id) : null })}
onClose={() => setAddModal(false)}
/>
)}
@@ -153,9 +214,9 @@ export default function TablesPage() {
{editModal && (
updateTable.mutate({ id: editModal.id, number: Number(f.number), label: f.label || null, group_id: f.group_id ? Number(f.group_id) : null })}
+ onSave={(f) => updateTable.mutate({ id: editModal.id, label: f.label || null, group_id: f.group_id ? Number(f.group_id) : null })}
onClose={() => setEditModal(null)}
/>
)}
@@ -163,18 +224,17 @@ export default function TablesPage() {
{/* Batch add */}
{batchModal !== null && (
batchCreate.mutate(body)}
onClose={() => setBatchModal(null)}
/>
)}
- {/* Group form */}
+ {/* Group/Zone form */}
{groupModal !== null && (
saveGroup.mutate({ name })}
+ onSave={(data) => saveGroup.mutate(data)}
onDelete={groupModal.id ? () => deleteGroup.mutate(groupModal.id) : null}
onClose={() => setGroupModal(null)}
/>
@@ -204,53 +264,63 @@ function TableModal({ title, initial, groups, onSave, onClose }) {
{title}
-
-
setForm(f => ({ ...f, number: e.target.value }))} autoFocus />
+
+
setForm(f => ({ ...f, label: e.target.value }))}
+ autoFocus
+ />
+
Αφήστε κενό για αυτόματη αρίθμηση.
-
- setForm(f => ({ ...f, label: e.target.value }))} />
-
-
-
+
Ακύρωση
- onSave(form)} disabled={!form.number} className="flex-1 btn btn-primary">Αποθήκευση
+ onSave(form)} className="flex-1 btn btn-primary">Αποθήκευση
)
}
-function BatchModal({ groupId, startNumber, onSave, onClose }) {
+function BatchModal({ group, onSave, onClose }) {
const [count, setCount] = useState(5)
- const [prefix, setPrefix] = useState('')
+ const [prefix, setPrefix] = useState(group?.prefix ? `${group.prefix}-` : '')
return (
Μαζική προσθήκη τραπεζιών
+ {group &&
Ζώνη: {group.name}
}
+
+
+
setPrefix(e.target.value)}
+ autoFocus
+ />
+
Τα ονόματα θα αριθμηθούν αυτόματα συνεχίζοντας από εκεί που σταμάτησαν.
+
- setCount(Number(e.target.value))} autoFocus />
+ setCount(Number(e.target.value))} />
-
-
- setPrefix(e.target.value)} />
-
-
Ξεκινά από αριθμό {startNumber}, δημιουργεί {count} τραπέζια.
Ακύρωση
onSave({ group_id: groupId, count, name_prefix: prefix, start_number: startNumber })}
+ onClick={() => onSave({ group_id: group?.id ?? null, count, name_prefix: prefix })}
disabled={count < 1 || !prefix.trim()}
className="flex-1 btn btn-primary"
>
- Δημιουργία
+ Δημιουργία {count > 0 && prefix.trim() ? `(${prefix.trim()}1 … ${prefix.trim()}${count})` : ''}
@@ -260,18 +330,29 @@ function BatchModal({ groupId, startNumber, onSave, onClose }) {
function GroupModal({ group, onSave, onDelete, onClose }) {
const [name, setName] = useState(group.name || '')
+ const [prefix, setPrefix] = useState(group.prefix || '')
+ const [color, setColor] = useState(group.color || null)
return (
-
-
{group.id ? 'Επεξεργασία γκρουπ' : 'Νέο γκρουπ'}
+
+
{group.id ? 'Επεξεργασία ζώνης' : 'Νέα ζώνη'}
-
- setName(e.target.value)} autoFocus />
+
+ setName(e.target.value)} autoFocus placeholder="π.χ. Beachside" />
+
+
+
+
setPrefix(e.target.value)} placeholder="π.χ. BS" />
+
Χρησιμοποιείται ως προτεινόμενο πρόθεμα στη μαζική προσθήκη.
+
+
+
+
- {onDelete && 🗑}
+ {onDelete && Διαγραφή}
Ακύρωση
- onSave(name)} disabled={!name.trim()} className="flex-1 btn btn-primary">Αποθήκευση
+ onSave({ name, prefix: prefix || null, color: color || null })} disabled={!name.trim()} className="flex-1 btn btn-primary">Αποθήκευση