Manager Dashboard: product reorder/bulk actions, preference sub-choices UI
ProductsPage: - Drag-free reorder via ▲/▼ buttons (calls PUT /products/reorder) - Sort toolbar: custom order / name / price - Show/hide inactive products toggle - Multi-select mode with bulk available/unavailable/hard-delete actions - Preference sets: default_choice_index picker, shared_subset editor (set-level sub-choices shared across choices, with disables_subset per choice) - Inline sub-choice editor for both checkbox options and preference choices (SubChoiceRows component with reorder, default toggle, price stepper) - Hard delete + soft deactivate distinction in the delete confirmation dialog - PriceInput component with −/+ steppers (0.10 increments) TablesPage: - Table groups gain prefix and color fields in create/edit form - Color swatch picker for group color Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,40 @@ import toast from 'react-hot-toast'
|
|||||||
import client from '../api/client'
|
import client from '../api/client'
|
||||||
import ConfirmModal from '../components/ConfirmModal'
|
import ConfirmModal from '../components/ConfirmModal'
|
||||||
|
|
||||||
|
const ZONE_COLORS = ['#6366f1','#0ea5e9','#10b981','#f59e0b','#ef4444','#ec4899','#8b5cf6','#14b8a6','#f97316','#64748b']
|
||||||
|
|
||||||
|
function ZoneColorPicker({ value, onChange }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(null)}
|
||||||
|
className="w-7 h-7 rounded-full border-2 bg-gray-200 transition-all"
|
||||||
|
style={{ borderColor: !value ? '#000' : 'transparent' }}
|
||||||
|
title="Χωρίς χρώμα"
|
||||||
|
/>
|
||||||
|
{ZONE_COLORS.map(c => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(c)}
|
||||||
|
className="w-7 h-7 rounded-full border-2 transition-all"
|
||||||
|
style={{ background: c, borderColor: value === c ? '#000' : 'transparent' }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function TablesPage() {
|
export default function TablesPage() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [addModal, setAddModal] = useState(false)
|
const [addModal, setAddModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(null)
|
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 [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 [showInactive, setShowInactive] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState('all') // 'all' | group.id
|
||||||
|
|
||||||
const { data: tables = [], isLoading } = useQuery({
|
const { data: tables = [], isLoading } = useQuery({
|
||||||
queryKey: ['tables-all', showInactive],
|
queryKey: ['tables-all', showInactive],
|
||||||
@@ -29,15 +55,6 @@ export default function TablesPage() {
|
|||||||
}
|
}
|
||||||
const invalidateGroups = () => qc.invalidateQueries({ queryKey: ['table-groups'] })
|
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({
|
const createTable = useMutation({
|
||||||
mutationFn: (body) => client.post('/api/tables/', body),
|
mutationFn: (body) => client.post('/api/tables/', body),
|
||||||
onSuccess: () => { toast.success('Τραπέζι δημιουργήθηκε'); setAddModal(false); invalidate() },
|
onSuccess: () => { toast.success('Τραπέζι δημιουργήθηκε'); setAddModal(false); invalidate() },
|
||||||
@@ -70,81 +87,125 @@ export default function TablesPage() {
|
|||||||
mutationFn: (body) => groupModal?.id
|
mutationFn: (body) => groupModal?.id
|
||||||
? client.put(`/api/tables/groups/${groupModal.id}`, body)
|
? client.put(`/api/tables/groups/${groupModal.id}`, body)
|
||||||
: client.post('/api/tables/groups', 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 || 'Σφάλμα'),
|
onError: (err) => toast.error(err.response?.data?.detail || 'Σφάλμα'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteGroup = useMutation({
|
const deleteGroup = useMutation({
|
||||||
mutationFn: (id) => client.delete(`/api/tables/groups/${id}`),
|
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('Σφάλμα'),
|
onError: () => toast.error('Σφάλμα'),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Group tables by group
|
// Filter tables for the active tab
|
||||||
const grouped = [
|
const visibleTables = activeTab === 'all'
|
||||||
{ group: null, tables: tables.filter(t => !t.group_id) },
|
? tables
|
||||||
...groups.map(g => ({ group: g, tables: tables.filter(t => t.group_id === g.id) })),
|
: activeTab === 'ungrouped'
|
||||||
].filter(section => section.tables.length > 0 || section.group)
|
? tables.filter(t => !t.group_id)
|
||||||
|
: tables.filter(t => t.group_id === activeTab)
|
||||||
|
|
||||||
if (isLoading) return <div className="flex items-center justify-center h-64 text-gray-400">Φόρτωση…</div>
|
if (isLoading) return <div className="flex items-center justify-center h-64 text-gray-400">Φόρτωση…</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-3xl">
|
<div className="space-y-4">
|
||||||
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||||
<h1 className="text-xl font-bold text-gray-800">Τραπέζια</h1>
|
<h1 className="text-xl font-bold text-gray-800">Τραπέζια</h1>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
<label className="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
|
<label className="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
|
||||||
<input type="checkbox" checked={showInactive} onChange={e => setShowInactive(e.target.checked)} className="accent-primary-700" />
|
<input type="checkbox" checked={showInactive} onChange={e => setShowInactive(e.target.checked)} className="accent-primary-700" />
|
||||||
Εμφάνιση ανενεργών
|
Εμφάνιση ανενεργών
|
||||||
</label>
|
</label>
|
||||||
<button onClick={() => setGroupModal({})} className="btn btn-secondary text-sm">+ Νέο γκρουπ</button>
|
<button onClick={() => setGroupModal({})} className="btn btn-secondary text-sm">+ Νέα ζώνη</button>
|
||||||
<button onClick={() => setAddModal(true)} className="btn btn-primary">+ Νέο τραπέζι</button>
|
<button onClick={() => setAddModal(true)} className="btn btn-primary text-sm">+ Νέο τραπέζι</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{grouped.map(({ group, tables: gt }) => (
|
{/* Zone tabs */}
|
||||||
<div key={group?.id ?? 'ungrouped'}>
|
<div className="flex gap-1 flex-wrap border-b border-gray-200 pb-0">
|
||||||
{group && (
|
{[
|
||||||
<div className="flex items-center gap-3 mb-2">
|
{ id: 'all', label: 'Όλα', color: null },
|
||||||
<h2 className="font-semibold text-gray-700">{group.name}</h2>
|
...groups.map(g => ({ id: g.id, label: g.prefix ? `${g.prefix} – ${g.name}` : g.name, color: g.color, group: g })),
|
||||||
<button onClick={() => setGroupModal(group)} className="text-xs text-gray-400 hover:text-gray-600">✏️</button>
|
...(tables.some(t => !t.group_id) ? [{ id: 'ungrouped', label: 'Χωρίς ζώνη', color: null }] : []),
|
||||||
<button onClick={() => setBatchModal(group.id)} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-7">+ Μαζική προσθήκη</button>
|
].map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => 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 && <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: tab.color }} />}
|
||||||
|
{tab.label}
|
||||||
|
<span className="ml-0.5 text-xs text-gray-400">
|
||||||
|
({tab.id === 'all' ? tables.length : tab.id === 'ungrouped' ? tables.filter(t => !t.group_id).length : tables.filter(t => t.group_id === tab.id).length})
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zone header (when viewing a specific zone) */}
|
||||||
|
{activeTab !== 'all' && activeTab !== 'ungrouped' && (() => {
|
||||||
|
const g = groups.find(g => g.id === activeTab)
|
||||||
|
if (!g) return null
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold text-gray-700">{g.name}</span>
|
||||||
|
{g.prefix && <span className="ml-2 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded font-mono">{g.prefix}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<button onClick={() => setGroupModal(g)} className="text-xs text-gray-400 hover:text-gray-600 underline">Επεξεργασία ζώνης</button>
|
||||||
{!group && gt.length > 0 && <h2 className="font-semibold text-gray-500 mb-2 text-sm">Χωρίς γκρουπ</h2>}
|
<button onClick={() => setBatchModal(g)} className="btn btn-secondary text-xs px-3 py-1 min-h-0 h-7">+ Μαζική προσθήκη</button>
|
||||||
|
|
||||||
<div className="card divide-y divide-gray-100">
|
|
||||||
{gt.length === 0 && (
|
|
||||||
<p className="px-4 py-4 text-sm text-gray-400 text-center">Δεν υπάρχουν τραπέζια σε αυτό το γκρουπ.</p>
|
|
||||||
)}
|
|
||||||
{gt.map(t => (
|
|
||||||
<div key={t.id} className={`flex items-center gap-4 px-4 py-3 ${!t.is_active ? 'opacity-40' : ''}`}>
|
|
||||||
<span className="text-2xl font-extrabold text-gray-800 w-10">{t.number}</span>
|
|
||||||
<p className="flex-1 text-sm text-gray-600">{t.label || '—'}</p>
|
|
||||||
{!t.is_active && <span className="text-xs text-red-400 font-medium">Ανενεργό</span>}
|
|
||||||
<button onClick={() => setEditModal(t)} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">Επεξεργασία</button>
|
|
||||||
{t.is_active
|
|
||||||
? <button onClick={() => 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">Απενεργ.</button>
|
|
||||||
: <button onClick={() => 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">Ενεργοπ.</button>
|
|
||||||
}
|
|
||||||
<button onClick={() => setConfirmDelete({ id: t.id, hard: true })} className="btn btn-danger text-sm px-3 py-1.5 min-h-0 h-9">Διαγραφή</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
))}
|
})()}
|
||||||
|
|
||||||
{tables.length === 0 && (
|
{/* Tables list */}
|
||||||
<p className="text-center text-gray-400 py-12">Δεν υπάρχουν τραπέζια. Προσθέστε ένα.</p>
|
<div className="card divide-y divide-gray-100">
|
||||||
)}
|
{visibleTables.length === 0 && (
|
||||||
|
<p className="px-4 py-8 text-sm text-gray-400 text-center">
|
||||||
|
{showInactive ? 'Δεν υπάρχουν τραπέζια.' : 'Δεν υπάρχουν ενεργά τραπέζια.'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{visibleTables.map((t, idx) => (
|
||||||
|
<div key={t.id} className={`flex items-center gap-4 px-4 py-3 ${!t.is_active ? 'opacity-50 bg-gray-50' : ''}`}>
|
||||||
|
<span className="text-xs text-gray-400 font-mono w-6 text-right">{idx + 1}</span>
|
||||||
|
<p className="flex-1 font-medium text-gray-800">{t.label || `Τραπέζι ${t.number}`}</p>
|
||||||
|
{t.group && (
|
||||||
|
<span className="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded hidden sm:inline">
|
||||||
|
{t.group.prefix ? `${t.group.prefix}` : t.group.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!t.is_active && <span className="text-xs text-amber-600 font-medium">Ανενεργό</span>}
|
||||||
|
<button onClick={() => setEditModal(t)} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-8">Επεξεργασία</button>
|
||||||
|
{t.is_active
|
||||||
|
? <button
|
||||||
|
onClick={() => !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"
|
||||||
|
>Απενεργ.</button>
|
||||||
|
: <button onClick={() => 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">Ενεργοπ.</button>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
onClick={() => !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"
|
||||||
|
>Διαγραφή</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Add single table */}
|
{/* Add single table */}
|
||||||
{addModal && (
|
{addModal && (
|
||||||
<TableModal
|
<TableModal
|
||||||
title="Νέο τραπέζι"
|
title="Νέο τραπέζι"
|
||||||
initial={{ number: nextNumber(null), label: '', group_id: '' }}
|
initial={{ label: '', group_id: activeTab !== 'all' && activeTab !== 'ungrouped' ? activeTab : '' }}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
onSave={(f) => 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)}
|
onClose={() => setAddModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -153,9 +214,9 @@ export default function TablesPage() {
|
|||||||
{editModal && (
|
{editModal && (
|
||||||
<TableModal
|
<TableModal
|
||||||
title="Επεξεργασία τραπεζιού"
|
title="Επεξεργασία τραπεζιού"
|
||||||
initial={{ number: editModal.number, label: editModal.label || '', group_id: editModal.group_id || '' }}
|
initial={{ label: editModal.label || '', group_id: editModal.group_id || '' }}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
onSave={(f) => 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)}
|
onClose={() => setEditModal(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -163,18 +224,17 @@ export default function TablesPage() {
|
|||||||
{/* Batch add */}
|
{/* Batch add */}
|
||||||
{batchModal !== null && (
|
{batchModal !== null && (
|
||||||
<BatchModal
|
<BatchModal
|
||||||
groupId={batchModal}
|
group={batchModal}
|
||||||
startNumber={nextNumber(batchModal)}
|
|
||||||
onSave={(body) => batchCreate.mutate(body)}
|
onSave={(body) => batchCreate.mutate(body)}
|
||||||
onClose={() => setBatchModal(null)}
|
onClose={() => setBatchModal(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Group form */}
|
{/* Group/Zone form */}
|
||||||
{groupModal !== null && (
|
{groupModal !== null && (
|
||||||
<GroupModal
|
<GroupModal
|
||||||
group={groupModal}
|
group={groupModal}
|
||||||
onSave={(name) => saveGroup.mutate({ name })}
|
onSave={(data) => saveGroup.mutate(data)}
|
||||||
onDelete={groupModal.id ? () => deleteGroup.mutate(groupModal.id) : null}
|
onDelete={groupModal.id ? () => deleteGroup.mutate(groupModal.id) : null}
|
||||||
onClose={() => setGroupModal(null)}
|
onClose={() => setGroupModal(null)}
|
||||||
/>
|
/>
|
||||||
@@ -204,53 +264,63 @@ function TableModal({ title, initial, groups, onSave, onClose }) {
|
|||||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm p-6 space-y-4">
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm p-6 space-y-4">
|
||||||
<h2 className="font-bold text-gray-800">{title}</h2>
|
<h2 className="font-bold text-gray-800">{title}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Αριθμός τραπεζιού *</label>
|
<label className="label">Όνομα τραπεζιού</label>
|
||||||
<input className="input" type="number" min="1" value={form.number} onChange={e => setForm(f => ({ ...f, number: e.target.value }))} autoFocus />
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="π.χ. BS-TBL-1 ή Βεράντα 3"
|
||||||
|
value={form.label}
|
||||||
|
onChange={e => setForm(f => ({ ...f, label: e.target.value }))}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Αφήστε κενό για αυτόματη αρίθμηση.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Ετικέτα</label>
|
<label className="label">Ζώνη</label>
|
||||||
<input className="input" placeholder="π.χ. Βεράντα 1" value={form.label} onChange={e => setForm(f => ({ ...f, label: e.target.value }))} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="label">Γκρουπ</label>
|
|
||||||
<select className="input" value={form.group_id} onChange={e => setForm(f => ({ ...f, group_id: e.target.value }))}>
|
<select className="input" value={form.group_id} onChange={e => setForm(f => ({ ...f, group_id: e.target.value }))}>
|
||||||
<option value="">— Χωρίς γκρουπ —</option>
|
<option value="">— Χωρίς ζώνη —</option>
|
||||||
{groups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
|
{groups.map(g => <option key={g.id} value={g.id}>{g.name}{g.prefix ? ` (${g.prefix})` : ''}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
|
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
|
||||||
<button onClick={() => onSave(form)} disabled={!form.number} className="flex-1 btn btn-primary">Αποθήκευση</button>
|
<button onClick={() => onSave(form)} className="flex-1 btn btn-primary">Αποθήκευση</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BatchModal({ groupId, startNumber, onSave, onClose }) {
|
function BatchModal({ group, onSave, onClose }) {
|
||||||
const [count, setCount] = useState(5)
|
const [count, setCount] = useState(5)
|
||||||
const [prefix, setPrefix] = useState('')
|
const [prefix, setPrefix] = useState(group?.prefix ? `${group.prefix}-` : '')
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm p-6 space-y-4">
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm p-6 space-y-4">
|
||||||
<h2 className="font-bold text-gray-800">Μαζική προσθήκη τραπεζιών</h2>
|
<h2 className="font-bold text-gray-800">Μαζική προσθήκη τραπεζιών</h2>
|
||||||
|
{group && <p className="text-sm text-gray-500">Ζώνη: <span className="font-medium text-gray-700">{group.name}</span></p>}
|
||||||
|
<div>
|
||||||
|
<label className="label">Πρόθεμα ονόματος</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="π.χ. BS-TBL- → BS-TBL-1, BS-TBL-2…"
|
||||||
|
value={prefix}
|
||||||
|
onChange={e => setPrefix(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Τα ονόματα θα αριθμηθούν αυτόματα συνεχίζοντας από εκεί που σταμάτησαν.</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Πλήθος</label>
|
<label className="label">Πλήθος</label>
|
||||||
<input className="input" type="number" min="1" max="200" value={count} onChange={e => setCount(Number(e.target.value))} autoFocus />
|
<input className="input" type="number" min="1" max="200" value={count} onChange={e => setCount(Number(e.target.value))} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="label">Πρόθεμα ετικέτας</label>
|
|
||||||
<input className="input" placeholder="π.χ. Out- → Out-1, Out-2…" value={prefix} onChange={e => setPrefix(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400">Ξεκινά από αριθμό {startNumber}, δημιουργεί {count} τραπέζια.</p>
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
|
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => 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()}
|
disabled={count < 1 || !prefix.trim()}
|
||||||
className="flex-1 btn btn-primary"
|
className="flex-1 btn btn-primary"
|
||||||
>
|
>
|
||||||
Δημιουργία
|
Δημιουργία {count > 0 && prefix.trim() ? `(${prefix.trim()}1 … ${prefix.trim()}${count})` : ''}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,18 +330,29 @@ function BatchModal({ groupId, startNumber, onSave, onClose }) {
|
|||||||
|
|
||||||
function GroupModal({ group, onSave, onDelete, onClose }) {
|
function GroupModal({ group, onSave, onDelete, onClose }) {
|
||||||
const [name, setName] = useState(group.name || '')
|
const [name, setName] = useState(group.name || '')
|
||||||
|
const [prefix, setPrefix] = useState(group.prefix || '')
|
||||||
|
const [color, setColor] = useState(group.color || null)
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-xs p-6 space-y-4">
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm p-6 space-y-4">
|
||||||
<h2 className="font-bold text-gray-800">{group.id ? 'Επεξεργασία γκρουπ' : 'Νέο γκρουπ'}</h2>
|
<h2 className="font-bold text-gray-800">{group.id ? 'Επεξεργασία ζώνης' : 'Νέα ζώνη'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Όνομα γκρουπ</label>
|
<label className="label">Όνομα ζώνης *</label>
|
||||||
<input className="input" value={name} onChange={e => setName(e.target.value)} autoFocus />
|
<input className="input" value={name} onChange={e => setName(e.target.value)} autoFocus placeholder="π.χ. Beachside" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Πρόθεμα (για μαζική δημιουργία)</label>
|
||||||
|
<input className="input font-mono" value={prefix} onChange={e => setPrefix(e.target.value)} placeholder="π.χ. BS" />
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Χρησιμοποιείται ως προτεινόμενο πρόθεμα στη μαζική προσθήκη.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Χρώμα ζώνης</label>
|
||||||
|
<ZoneColorPicker value={color} onChange={setColor} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{onDelete && <button onClick={onDelete} className="btn btn-danger px-3">🗑</button>}
|
{onDelete && <button onClick={onDelete} className="btn btn-danger px-3">Διαγραφή</button>}
|
||||||
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
|
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
|
||||||
<button onClick={() => onSave(name)} disabled={!name.trim()} className="flex-1 btn btn-primary">Αποθήκευση</button>
|
<button onClick={() => onSave({ name, prefix: prefix || null, color: color || null })} disabled={!name.trim()} className="flex-1 btn btn-primary">Αποθήκευση</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user