import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import toast from 'react-hot-toast' import client from '../api/client' import ConfirmModal from '../components/ConfirmModal' const EMPTY_PRODUCT = { name: '', category_id: '', base_price: '', is_available: true, lifecycle_status: 'active', printer_zone_id: '', quick_options: [], options: [], ingredients: [], preference_sets: [], } const COLORS = ['#6366f1','#0ea5e9','#10b981','#f59e0b','#ef4444','#ec4899','#8b5cf6','#14b8a6','#f97316','#64748b'] function ColorPicker({ value, onChange }) { return (
{COLORS.map(c => (
) } function IconBase({ className = '', viewBox, strokeWidth = '1.5', children }) { return ( ) } function AddIcon({ className = '' }) { return ( ) } function MoveUpIcon({ className = '' }) { return ( ) } function MoveDownIcon({ className = '' }) { return ( ) } function EditIcon({ className = '' }) { return ( ) } function DeleteIcon({ className = '' }) { return ( ) } function HeartIcon({ filled, className = '' }) { return ( ) } function ReorderBtns({ onUp, onDown, disableUp, disableDown }) { return (
) } function DefaultBtn({ isDefault, onClick, title }) { return ( ) } function FavoriteBtn({ isFavorite, onClick, title }) { return ( ) } function PriceInput({ value, onChange, placeholder, className = '', allowNegative = false }) { const step = 0.10 const num = parseFloat(value) || 0 function inc() { onChange(Math.round((num + step) * 100) / 100) } function dec() { const next = Math.round((num - step) * 100) / 100 onChange(allowNegative ? next : Math.max(0, next)) } return (
onChange(e.target.value)} placeholder={placeholder ?? '0.00'} className="flex-1 min-w-0 text-center text-sm outline-none bg-transparent px-1" />
) } function moveItem(arr, i, dir) { const j = i + dir if (j < 0 || j >= arr.length) return arr const next = [...arr] ;[next[i], next[j]] = [next[j], next[i]] return next } function SubChoiceRows({ subChoices, onMove, onToggleDefault, onChange, onRemove, onAdd, parentLabel }) { if (!subChoices || subChoices.length === 0) return null return (

Υπο-επιλογές του «{parentLabel || '…'}» — εμφανίζονται μόνο αν επιλεγεί

{subChoices.map((sc, sci) => (
onMove(sci, -1)} onDown={() => onMove(sci, 1)} disableUp={sci === 0} disableDown={sci === subChoices.length - 1} /> onToggleDefault(sci)} /> onChange(sci, 'name', e.target.value)} /> onChange(sci, 'extra_cost', v)} allowNegative className="w-28 text-sm" />
))}
) } // Build the ordered list of sub-category-like rows for a parent: // interleaves the "General" virtual row at its general_sort_order position function buildSubList(parent, subcategories) { const subs = [...subcategories].sort((a, b) => a.sort_order - b.sort_order) const generalRow = { _isGeneral: true, sort_order: parent.general_sort_order } const all = [...subs, generalRow].sort((a, b) => a.sort_order - b.sort_order) return all } export default function ProductsPage() { const qc = useQueryClient() const [selectedCat, setSelectedCat] = useState(null) const [editProduct, setEditProduct] = useState(null) const [editCat, setEditCat] = useState(null) const [confirmDelete, setConfirmDelete] = useState(null) const [showInactive, setShowInactive] = useState(false) const [showArchived, setShowArchived] = useState(false) const [multiSelect, setMultiSelect] = useState(false) const [selected, setSelected] = useState(new Set()) const [sortMode, setSortMode] = useState('custom') const { data: categories = [] } = useQuery({ queryKey: ['categories'], queryFn: () => client.get('/api/products/categories').then(r => r.data), }) const { data: allProducts = [] } = useQuery({ queryKey: ['products-all'], queryFn: () => client.get('/api/products/?all=true').then(r => r.data), }) const { data: statusData } = useQuery({ queryKey: ['system-status'], queryFn: () => client.get('/api/system/status').then(r => r.data), staleTime: 60_000, }) const printers = statusData?.printers ?? [] // selectedCat can be: // null → all products // number → specific category (if top-level: includes all sub-cats too) // '__general_' → only direct products of that top-level category (no sub-cat) const isGeneralSel = typeof selectedCat === 'string' && selectedCat.startsWith('__general_') const generalParentId = isGeneralSel ? Number(selectedCat.replace('__general_', '')) : null const selectedCatObj = isGeneralSel ? null : categories.find(c => c.id === selectedCat) const visibleCatIds = isGeneralSel ? [generalParentId] // only direct products on the parent : selectedCat ? selectedCatObj?.parent_id == null ? [selectedCat, ...categories.filter(c => c.parent_id === selectedCat).map(c => c.id)] : [selectedCat] : null const filteredByCat = visibleCatIds ? allProducts.filter(p => visibleCatIds.includes(p.category_id)) : allProducts const baseList = filteredByCat.filter(p => { if (p.lifecycle_status === 'archived') return showArchived if (!p.is_available) return showInactive return true }) const products = [...baseList].sort((a, b) => { if (sortMode === 'name') return a.name.localeCompare(b.name, 'el') if (sortMode === 'price') return a.base_price - b.base_price return (a.sort_order ?? 0) - (b.sort_order ?? 0) || a.id - b.id }) const invalidate = () => { qc.invalidateQueries({ queryKey: ['products-all'] }) qc.invalidateQueries({ queryKey: ['categories'] }) } const saveCat = useMutation({ mutationFn: b => editCat?.id ? client.put(`/api/products/categories/${editCat.id}`, b) : client.post('/api/products/categories', b), onSuccess: () => { toast.success('Κατηγορία αποθηκεύτηκε'); setEditCat(null); invalidate() }, onError: () => toast.error('Σφάλμα'), }) const deleteCat = useMutation({ mutationFn: id => client.delete(`/api/products/categories/${id}`), onSuccess: () => { toast.success('Διαγράφηκε'); setConfirmDelete(null); invalidate() }, onError: () => toast.error('Σφάλμα'), }) const reorderCats = useMutation({ mutationFn: items => client.put('/api/products/categories/reorder', items), onSuccess: () => invalidate(), }) const reorderSubcats = useMutation({ mutationFn: items => client.put('/api/products/categories/reorder-subcategories', items), onSuccess: () => invalidate(), }) const reorderGeneral = useMutation({ mutationFn: items => client.put('/api/products/categories/reorder-general', items), onSuccess: () => invalidate(), }) const toggleAutoExpanded = useMutation({ mutationFn: ({ id, auto_expanded }) => client.put(`/api/products/categories/${id}`, { auto_expanded }), onSuccess: () => invalidate(), }) const saveProduct = useMutation({ mutationFn: b => editProduct?.id ? client.put(`/api/products/${editProduct.id}`, b) : client.post('/api/products/', b), onSuccess: () => { toast.success('Προϊόν αποθηκεύτηκε'); setEditProduct(null); invalidate() }, onError: () => toast.error('Σφάλμα'), }) const toggleAvail = useMutation({ mutationFn: ({ id, is_available }) => client.put(`/api/products/${id}`, { is_available }), onSuccess: () => invalidate(), onError: () => toast.error('Σφάλμα'), }) const archiveProduct = useMutation({ mutationFn: id => client.delete(`/api/products/${id}`), onSuccess: () => { toast.success('Αρχειοθετήθηκε'); setConfirmDelete(null); invalidate() }, onError: () => toast.error('Σφάλμα'), }) const hardDeleteProduct = useMutation({ mutationFn: id => client.delete(`/api/products/${id}?hard=true`), onSuccess: () => { toast.success('Διαγράφηκε οριστικά'); setConfirmDelete(null); invalidate() }, onError: err => toast.error(err.response?.data?.detail || 'Σφάλμα'), }) const reorderProducts = useMutation({ mutationFn: items => client.put('/api/products/reorder', items), onSuccess: () => invalidate(), }) function moveProd(prod, dir) { const sorted = [...baseList].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || a.id - b.id) const idx = sorted.findIndex(p => p.id === prod.id) const swapIdx = idx + dir if (swapIdx < 0 || swapIdx >= sorted.length) return const normalised = sorted.map((p, i) => ({ id: p.id, sort_order: i })) ;[normalised[idx].sort_order, normalised[swapIdx].sort_order] = [normalised[swapIdx].sort_order, normalised[idx].sort_order] reorderProducts.mutate(normalised) } function moveCat(cat, dir) { const sorted = categories.filter(c => !c.parent_id).sort((a, b) => a.sort_order - b.sort_order) const idx = sorted.findIndex(c => c.id === cat.id) const swapIdx = idx + dir if (swapIdx < 0 || swapIdx >= sorted.length) return const updates = sorted.map((c, i) => { if (i === idx) return { id: c.id, sort_order: sorted[swapIdx].sort_order } if (i === swapIdx) return { id: c.id, sort_order: sorted[idx].sort_order } return { id: c.id, sort_order: c.sort_order } }) reorderCats.mutate(updates) } // Shared helper: move any row (sub-cat or General) in the mixed sub-list. // Normalises the whole list to 0-based sequential indices, swaps the two positions, // then writes all updated values. This avoids colliding sort_order values. function _moveInSubList(parent, itemIdx, dir) { const subs = categories.filter(c => c.parent_id === parent.id).sort((a, b) => a.sort_order - b.sort_order) const subList = buildSubList(parent, subs) const swapIdx = itemIdx + dir if (itemIdx < 0 || swapIdx < 0 || swapIdx >= subList.length) return // Normalise to clean sequential indices const normalised = subList.map((row, i) => ({ ...row, _normOrder: i })) ;[normalised[itemIdx]._normOrder, normalised[swapIdx]._normOrder] = [normalised[swapIdx]._normOrder, normalised[itemIdx]._normOrder] // Write subcats and general separately const subcatUpdates = normalised .filter(r => !r._isGeneral) .map(r => ({ id: r.id, sort_order: r._normOrder })) const generalRow = normalised.find(r => r._isGeneral) if (subcatUpdates.length) reorderSubcats.mutate(subcatUpdates) if (generalRow) reorderGeneral.mutate([{ id: parent.id, general_sort_order: generalRow._normOrder }]) } function moveSubcat(parent, subcat, dir) { const subs = categories.filter(c => c.parent_id === parent.id).sort((a, b) => a.sort_order - b.sort_order) const subList = buildSubList(parent, subs) const idx = subList.findIndex(r => !r._isGeneral && r.id === subcat.id) _moveInSubList(parent, idx, dir) } function moveGeneral(parent, dir) { const subs = categories.filter(c => c.parent_id === parent.id).sort((a, b) => a.sort_order - b.sort_order) const subList = buildSubList(parent, subs) const idx = subList.findIndex(r => r._isGeneral) _moveInSubList(parent, idx, dir) } function toggleSelect(id) { setSelected(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n }) } function selectAll() { setSelected(new Set(products.map(p => p.id))) } function clearSelect() { setSelected(new Set()) } function exitMultiSelect() { setMultiSelect(false); setSelected(new Set()) } async function bulkAction(action) { const ids = [...selected] if (!ids.length) return try { if (action === 'available') { await Promise.all(ids.map(id => client.put(`/api/products/${id}`, { is_available: true }))); toast.success(`${ids.length} διαθέσιμα`) } else if (action === 'unavailable') { await Promise.all(ids.map(id => client.put(`/api/products/${id}`, { is_available: false }))); toast.success(`${ids.length} μη διαθέσιμα`) } else if (action === 'archive') { setConfirmDelete({ type: 'bulk-archive', ids }); return } invalidate(); exitMultiSelect() } catch { toast.error('Σφάλμα') } } async function confirmBulkArchive(ids) { const results = await Promise.allSettled(ids.map(id => client.delete(`/api/products/${id}`))) const ok = results.filter(r => r.status === 'fulfilled').length const fail = results.filter(r => r.status === 'rejected').length if (ok) toast.success(`${ok} αρχειοθετήθηκαν ή διαγράφηκαν`) if (fail) toast.error(`${fail} απέτυχαν`) setConfirmDelete(null); invalidate(); exitMultiSelect() } function handleConfirmDelete() { if (!confirmDelete) return if (confirmDelete.type === 'category') deleteCat.mutate(confirmDelete.id) if (confirmDelete.type === 'product-archive') archiveProduct.mutate(confirmDelete.id) if (confirmDelete.type === 'product-hard') hardDeleteProduct.mutate(confirmDelete.id) if (confirmDelete.type === 'bulk-archive') confirmBulkArchive(confirmDelete.ids) } const printerName = id => printers.find(p => p.id === id)?.name ?? `#${id}` const categoryIconClass = 'w-4 h-4' const categoryActionBtnClass = 'p-1 rounded-md hover:bg-black/5 disabled:opacity-30 disabled:cursor-not-allowed' const categoryMoveBtnClass = `${categoryActionBtnClass} text-primary-700 hover:text-primary-800` const categoryEditBtnClass = `${categoryActionBtnClass} text-orange-500 hover:text-orange-600` const categoryDeleteBtnClass = `${categoryActionBtnClass} text-red-600 hover:text-red-700` const topLevelCats = categories.filter(c => !c.parent_id).sort((a, b) => a.sort_order - b.sort_order) return (
{/* Left: Categories */} {/* Right: Products */}

{isGeneralSel ? (() => { const parent = categories.find(c => c.id === generalParentId) return `Προϊόντα — ${parent?.name ?? '?'} / Γενικά` })() : selectedCat ? (() => { const cat = categories.find(c => c.id === selectedCat) if (!cat) return 'Προϊόντα' if (cat.parent_id) { const parent = categories.find(c => c.id === cat.parent_id) return `Προϊόντα — ${parent?.name ?? '?'} / ${cat.name}` } return `Προϊόντα — ${cat.name}` })() : 'Προϊόντα' }

{!multiSelect && } {multiSelect && (
{selected.size} επιλεγμένα
)} {!multiSelect && }
{products.length === 0 &&

Δεν υπάρχουν προϊόντα.

}
{products.map((p, idx) => { const isArchived = p.lifecycle_status === 'archived' return (
toggleSelect(p.id) : undefined} className={`card p-4 flex items-center gap-3 transition-opacity ${(!p.is_available || isArchived) ? 'opacity-60' : ''} ${multiSelect ? 'cursor-pointer select-none' : ''} ${multiSelect && selected.has(p.id) ? 'ring-2 ring-primary-500 bg-primary-50' : ''}`} > {multiSelect && ( toggleSelect(p.id)} onClick={e => e.stopPropagation()} className="w-4 h-4 accent-primary-700 shrink-0" /> )} {p.image_url && ( {p.name} )}

{p.name} {isArchived && ( Αρχείο )} {!isArchived && !p.is_available && ( Ανενεργό )}

{(() => { const cat = categories.find(c => c.id === p.category_id) if (!cat) return '—' if (cat.parent_id) { const parent = categories.find(c => c.id === cat.parent_id) return `${parent?.name ?? '?'} / ${cat.name}` } return cat.name })()} · €{p.base_price.toFixed(2)} {p.printer_zone_id && ` · ${printerName(p.printer_zone_id)}`}

{!multiSelect && ( <> {/* Availability toggle — only for non-archived products */} {!isArchived && ( )} {/* Archive/delete button — context-sensitive */} {isArchived ? ( ) : ( )} {sortMode === 'custom' && !isArchived && (
)} )}
) })}
{editCat !== null && ( c.id === editCat.parent_id)?.name : null} onSave={(name, color) => saveCat.mutate({ name, color, parent_id: editCat.parent_id ?? null })} onClose={() => setEditCat(null)} /> )} {editProduct !== null && ( saveProduct.mutate(b)} onCopy={formData => setEditProduct({ ...EMPTY_PRODUCT, ...formData, id: undefined, image_url: undefined, name: formData.name + ' (αντίγραφο)' })} onClose={() => setEditProduct(null)} /> )} {confirmDelete && ( setConfirmDelete(null)} /> )}
) } function CategoryFormModal({ cat, parentName, onSave, onClose }) { const [name, setName] = useState(cat.name || '') const [color, setColor] = useState(cat.color || '') const isNew = !cat.id const isSub = !!cat.parent_id const title = isNew ? isSub ? `Νέα υποκατηγορία${parentName ? ` σε «${parentName}»` : ''}` : 'Νέα κατηγορία' : isSub ? 'Επεξεργασία υποκατηγορίας' : 'Επεξεργασία κατηγορίας' return (

{title}

{isSub && parentName && (

Κατηγορία: {parentName}

)}
setName(e.target.value)} autoFocus />
) } // ─── Product Form Modal ─────────────────────────────────────────────────────── function buildFormFromProduct(product) { return { name: product.name || '', category_id: product.category_id ?? '', base_price: product.base_price ?? '', is_available: product.is_available ?? true, lifecycle_status: product.lifecycle_status ?? 'active', printer_zone_id: product.printer_zone_id ?? '', quick_options: product.quick_options?.map(q => ({ name: q.name, price: q.price ?? 0, allow_multiple: q.allow_multiple ?? false, sort_order: q.sort_order ?? 0, is_favorite: q.is_favorite ?? false, favorite_sort_order: q.favorite_sort_order ?? 0, })) ?? [], options: product.options?.map(o => ({ name: o.name, extra_cost: o.extra_cost ?? 0, allow_multiple: o.allow_multiple ?? false, sub_choices: o.sub_choices?.map(s => ({ name: s.name, extra_cost: s.extra_cost ?? 0, is_default: s.is_default ?? false })) ?? [], is_favorite: o.is_favorite ?? false, favorite_sort_order: o.favorite_sort_order ?? 0, })) ?? [], ingredients: product.ingredients?.map(i => ({ name: i.name, extra_cost: i.extra_cost ?? 0, is_favorite: i.is_favorite ?? false, favorite_sort_order: i.favorite_sort_order ?? 0, })) ?? [], preference_sets: product.preference_sets?.map(ps => ({ name: ps.name, default_choice_index: ps.choices ? ps.choices.findIndex(c => c.id === ps.default_choice_id) : -1, choices: ps.choices?.map(c => ({ name: c.name, extra_cost: c.extra_cost ?? 0, disables_subset: c.disables_subset ?? false, sub_choices: c.sub_choices?.map(s => ({ name: s.name, extra_cost: s.extra_cost ?? 0, is_default: s.is_default ?? false })) ?? [], })) ?? [], shared_subset: ps.shared_subset ? { name: ps.shared_subset.name, choices: ps.shared_subset.choices?.map(s => ({ name: s.name, extra_cost: s.extra_cost ?? 0, is_default: s.is_default ?? false })) ?? [], } : null, is_favorite: ps.is_favorite ?? false, favorite_sort_order: ps.favorite_sort_order ?? 0, })) ?? [], } } // Build a flat sorted list of all favorited items across all types function buildFavoritesList(form) { const items = [] form.quick_options.forEach((q, i) => { if (q.is_favorite) items.push({ type: 'quick', idx: i, favorite_sort_order: q.favorite_sort_order ?? 0 }) }) form.ingredients.forEach((ing, i) => { if (ing.is_favorite) items.push({ type: 'ingredient', idx: i, favorite_sort_order: ing.favorite_sort_order ?? 0 }) }) form.options.forEach((o, i) => { if (o.is_favorite) items.push({ type: 'option', idx: i, favorite_sort_order: o.favorite_sort_order ?? 0 }) }) form.preference_sets.forEach((ps, i) => { if (ps.is_favorite) items.push({ type: 'pref', idx: i, favorite_sort_order: ps.favorite_sort_order ?? 0 }) }) return items.sort((a, b) => a.favorite_sort_order - b.favorite_sort_order) } function getFavSortField(form, type, idx) { if (type === 'quick') return form.quick_options[idx]?.favorite_sort_order ?? 0 if (type === 'ingredient') return form.ingredients[idx]?.favorite_sort_order ?? 0 if (type === 'option') return form.options[idx]?.favorite_sort_order ?? 0 if (type === 'pref') return form.preference_sets[idx]?.favorite_sort_order ?? 0 return 0 } function setFavSortField(form, type, idx, value) { if (type === 'quick') return { ...form, quick_options: form.quick_options.map((q, i) => i === idx ? { ...q, favorite_sort_order: value } : q) } if (type === 'ingredient') return { ...form, ingredients: form.ingredients.map((ing, i) => i === idx ? { ...ing, favorite_sort_order: value } : ing) } if (type === 'option') return { ...form, options: form.options.map((o, i) => i === idx ? { ...o, favorite_sort_order: value } : o) } if (type === 'pref') return { ...form, preference_sets: form.preference_sets.map((ps, i) => i === idx ? { ...ps, favorite_sort_order: value } : ps) } return form } function getItemLabel(form, type, idx) { if (type === 'quick') return form.quick_options[idx]?.name || '(χωρίς όνομα)' if (type === 'ingredient') return form.ingredients[idx]?.name || '(χωρίς όνομα)' if (type === 'option') return form.options[idx]?.name || '(χωρίς όνομα)' if (type === 'pref') return form.preference_sets[idx]?.name || '(χωρίς όνομα)' return '' } function getItemTypeLabel(type) { if (type === 'quick') return 'Γρήγορη' if (type === 'ingredient') return 'Υλικό' if (type === 'option') return 'Έξτρα' if (type === 'pref') return 'Προτίμηση' return '' } function ProductFormModal({ product, categories, printers, onSave, onCopy, onClose }) { const [form, setForm] = useState(() => buildFormFromProduct(product)) const [activeTab, setActiveTab] = useState('favorites') const [imageFile, setImageFile] = useState(null) const [uploading, setUploading] = useState(false) const qc = useQueryClient() useEffect(() => { function onKey(e) { if (e.key === 'Escape') onClose() } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [onClose]) useEffect(() => { setForm(buildFormFromProduct(product)) setActiveTab('favorites') setImageFile(null) }, [product.id, product.name]) function setField(k, v) { setForm(f => ({ ...f, [k]: v })) } // ── Favorites reorder ── function moveFavorite(favList, favIdx, dir) { const newList = [...favList] const swapIdx = favIdx + dir if (swapIdx < 0 || swapIdx >= newList.length) return // Swap favorite_sort_order values const aOrder = newList[favIdx].favorite_sort_order const bOrder = newList[swapIdx].favorite_sort_order setForm(f => { let next = setFavSortField(f, newList[favIdx].type, newList[favIdx].idx, bOrder) next = setFavSortField(next, newList[swapIdx].type, newList[swapIdx].idx, aOrder) return next }) } function toggleFavorite(type, idx) { setForm(f => { const currentFavs = buildFavoritesList(f) const isFav = (() => { if (type === 'quick') return f.quick_options[idx]?.is_favorite if (type === 'ingredient') return f.ingredients[idx]?.is_favorite if (type === 'option') return f.options[idx]?.is_favorite if (type === 'pref') return f.preference_sets[idx]?.is_favorite })() // Assign next available sort order when adding const newSortOrder = isFav ? 0 : (currentFavs.length > 0 ? Math.max(...currentFavs.map(x => x.favorite_sort_order)) + 1 : 0) if (type === 'quick') return { ...f, quick_options: f.quick_options.map((q, i) => i === idx ? { ...q, is_favorite: !isFav, favorite_sort_order: isFav ? 0 : newSortOrder } : q) } if (type === 'ingredient') return { ...f, ingredients: f.ingredients.map((ing, i) => i === idx ? { ...ing, is_favorite: !isFav, favorite_sort_order: isFav ? 0 : newSortOrder } : ing) } if (type === 'option') return { ...f, options: f.options.map((o, i) => i === idx ? { ...o, is_favorite: !isFav, favorite_sort_order: isFav ? 0 : newSortOrder } : o) } if (type === 'pref') return { ...f, preference_sets: f.preference_sets.map((ps, i) => i === idx ? { ...ps, is_favorite: !isFav, favorite_sort_order: isFav ? 0 : newSortOrder } : ps) } return f }) } // ── Quick Options ── function addQuickOption() { setForm(f => ({ ...f, quick_options: [...f.quick_options, { name: '', price: 0, allow_multiple: false, sort_order: f.quick_options.length, is_favorite: false, favorite_sort_order: 0 }] })) } function removeQuickOption(i) { setForm(f => ({ ...f, quick_options: f.quick_options.filter((_, idx) => idx !== i) })) } function setQuickOption(i, k, v) { setForm(f => ({ ...f, quick_options: f.quick_options.map((q, idx) => idx === i ? { ...q, [k]: v } : q) })) } function moveQuickOption(i, dir) { setForm(f => ({ ...f, quick_options: moveItem(f.quick_options, i, dir) })) } // ── Options ── function addOption() { setForm(f => ({ ...f, options: [...f.options, { name: '', extra_cost: 0, allow_multiple: false, sub_choices: [], is_favorite: false, favorite_sort_order: 0 }] })) } function removeOption(i) { setForm(f => ({ ...f, options: f.options.filter((_, idx) => idx !== i) })) } function setOption(i, k, v) { setForm(f => ({ ...f, options: f.options.map((o, idx) => idx === i ? { ...o, [k]: v } : o) })) } function moveOption(i, dir) { setForm(f => ({ ...f, options: moveItem(f.options, i, dir) })) } function addOptionSubChoice(oi) { setForm(f => ({ ...f, options: f.options.map((o, idx) => idx !== oi ? o : { ...o, sub_choices: [...(o.sub_choices || []), { name: '', extra_cost: 0, is_default: false }] } )})) } function removeOptionSubChoice(oi, sci) { setForm(f => ({ ...f, options: f.options.map((o, idx) => idx !== oi ? o : { ...o, sub_choices: o.sub_choices.filter((_, i) => i !== sci) } )})) } function setOptionSubChoice(oi, sci, k, v) { setForm(f => ({ ...f, options: f.options.map((o, idx) => idx !== oi ? o : { ...o, sub_choices: o.sub_choices.map((sc, scidx) => { if (scidx !== sci) return k === 'is_default' && v === true ? { ...sc, is_default: false } : sc return { ...sc, [k]: v } }) } )})) } function moveOptionSubChoice(oi, sci, dir) { setForm(f => ({ ...f, options: f.options.map((o, idx) => idx !== oi ? o : { ...o, sub_choices: moveItem(o.sub_choices, sci, dir) } )})) } function toggleOptionSubDefault(oi, sci) { const sc = form.options[oi]?.sub_choices?.[sci] setOptionSubChoice(oi, sci, 'is_default', !sc?.is_default) } // ── Ingredients ── function addIngredient() { setForm(f => ({ ...f, ingredients: [...f.ingredients, { name: '', extra_cost: 0, is_favorite: false, favorite_sort_order: 0 }] })) } function removeIngredient(i) { setForm(f => ({ ...f, ingredients: f.ingredients.filter((_, idx) => idx !== i) })) } function setIngredient(i, k, v) { setForm(f => ({ ...f, ingredients: f.ingredients.map((ing, idx) => idx === i ? { ...ing, [k]: v } : ing) })) } function moveIngredient(i, dir) { setForm(f => ({ ...f, ingredients: moveItem(f.ingredients, i, dir) })) } // ── Preference sets ── function addPrefSet() { const newIdx = form.preference_sets.length setForm(f => ({ ...f, preference_sets: [...f.preference_sets, { name: '', default_choice_index: -1, choices: [], shared_subset: null, is_favorite: false, favorite_sort_order: 0 }] })) setActiveTab(newIdx) } function removePrefSet(si) { setForm(f => ({ ...f, preference_sets: f.preference_sets.filter((_, idx) => idx !== si) })) setActiveTab('favorites') } function setPrefSetField(si, k, v) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => idx === si ? { ...ps, [k]: v } : ps) })) } function addChoice(si) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => idx === si ? { ...ps, choices: [...ps.choices, { name: '', extra_cost: 0, disables_subset: false, sub_choices: [] }] } : ps )})) } function removeChoice(si, ci) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => { if (idx !== si) return ps const newChoices = ps.choices.filter((_, cidx) => cidx !== ci) const d = ps.default_choice_index const newDefault = d === ci ? -1 : d > ci ? d - 1 : d return { ...ps, choices: newChoices, default_choice_index: newDefault } })})) } function setChoice(si, ci, k, v) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => idx === si ? { ...ps, choices: ps.choices.map((ch, cidx) => cidx === ci ? { ...ch, [k]: v } : ch) } : ps )})) } function moveChoice(si, ci, dir) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => { if (idx !== si) return ps const newChoices = moveItem(ps.choices, ci, dir) const j = ci + dir let nd = ps.default_choice_index if (nd === ci) nd = j; else if (nd === j) nd = ci return { ...ps, choices: newChoices, default_choice_index: nd } })})) } function toggleDefaultChoice(si, ci) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => idx !== si ? ps : { ...ps, default_choice_index: ps.default_choice_index === ci ? -1 : ci } )})) } function addSubChoice(si, ci) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, pidx) => pidx !== si ? ps : { ...ps, choices: ps.choices.map((ch, cidx) => cidx !== ci ? ch : { ...ch, sub_choices: [...(ch.sub_choices || []), { name: '', extra_cost: 0, is_default: false }] } )} )})) } function removeSubChoice(si, ci, sci) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, pidx) => pidx !== si ? ps : { ...ps, choices: ps.choices.map((ch, cidx) => cidx !== ci ? ch : { ...ch, sub_choices: ch.sub_choices.filter((_, i) => i !== sci) } )} )})) } function setSubChoice(si, ci, sci, k, v) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, pidx) => pidx !== si ? ps : { ...ps, choices: ps.choices.map((ch, cidx) => cidx !== ci ? ch : { ...ch, sub_choices: ch.sub_choices.map((sc, scidx) => { if (scidx !== sci) return k === 'is_default' && v === true ? { ...sc, is_default: false } : sc return { ...sc, [k]: v } })} )} )})) } function moveSubChoice(si, ci, sci, dir) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, pidx) => pidx !== si ? ps : { ...ps, choices: ps.choices.map((ch, cidx) => cidx !== ci ? ch : { ...ch, sub_choices: moveItem(ch.sub_choices, sci, dir) } )} )})) } function setSharedSubsetName(si, name) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => { if (idx !== si) return ps return { ...ps, shared_subset: ps.shared_subset ? { ...ps.shared_subset, name } : { name, choices: [] } } })})) } function addSharedSubsetChoice(si) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => { if (idx !== si) return ps const ss = ps.shared_subset || { name: '', choices: [] } return { ...ps, shared_subset: { ...ss, choices: [...ss.choices, { name: '', extra_cost: 0, is_default: false }] } } })})) } function removeSharedSubsetChoice(si, sci) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => { if (idx !== si) return ps const newChoices = ps.shared_subset.choices.filter((_, i) => i !== sci) return { ...ps, shared_subset: newChoices.length ? { ...ps.shared_subset, choices: newChoices } : null } })})) } function setSharedSubsetChoice(si, sci, k, v) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => { if (idx !== si) return ps const newChoices = ps.shared_subset.choices.map((sc, scidx) => { if (scidx !== sci) return k === 'is_default' && v === true ? { ...sc, is_default: false } : sc return { ...sc, [k]: v } }) return { ...ps, shared_subset: { ...ps.shared_subset, choices: newChoices } } })})) } function moveSharedSubsetChoice(si, sci, dir) { setForm(f => ({ ...f, preference_sets: f.preference_sets.map((ps, idx) => idx !== si ? ps : { ...ps, shared_subset: { ...ps.shared_subset, choices: moveItem(ps.shared_subset.choices, sci, dir) } } )})) } function buildBody() { return { name: form.name, category_id: form.category_id ? Number(form.category_id) : null, base_price: parseFloat(form.base_price), is_available: form.is_available, lifecycle_status: form.lifecycle_status, printer_zone_id: form.printer_zone_id ? Number(form.printer_zone_id) : null, quick_options: form.quick_options.map((q, i) => ({ name: q.name, price: parseFloat(q.price) || 0, allow_multiple: q.allow_multiple ?? false, sort_order: i, is_favorite: q.is_favorite ?? false, favorite_sort_order: q.favorite_sort_order ?? 0, })), options: form.options.map(o => ({ name: o.name, extra_cost: parseFloat(o.extra_cost) || 0, allow_multiple: o.allow_multiple ?? false, sub_choices: (o.sub_choices || []).map(s => ({ name: s.name, extra_cost: parseFloat(s.extra_cost) || 0, is_default: s.is_default ?? false })), is_favorite: o.is_favorite ?? false, favorite_sort_order: o.favorite_sort_order ?? 0, })), ingredients: form.ingredients.map(i => ({ name: i.name, extra_cost: parseFloat(i.extra_cost) || 0, is_favorite: i.is_favorite ?? false, favorite_sort_order: i.favorite_sort_order ?? 0, })), preference_sets: form.preference_sets.map(ps => ({ name: ps.name, default_choice_index: ps.default_choice_index >= 0 ? ps.default_choice_index : null, shared_subset: ps.shared_subset?.choices?.length ? { name: ps.shared_subset.name || '', choices: ps.shared_subset.choices.map(s => ({ name: s.name, extra_cost: parseFloat(s.extra_cost) || 0, is_default: s.is_default ?? false })), } : null, choices: ps.choices.map(c => ({ name: c.name, extra_cost: parseFloat(c.extra_cost) || 0, disables_subset: c.disables_subset ?? false, sub_choices: (c.sub_choices || []).map(s => ({ name: s.name, extra_cost: parseFloat(s.extra_cost) || 0, is_default: s.is_default ?? false })), })), is_favorite: ps.is_favorite ?? false, favorite_sort_order: ps.favorite_sort_order ?? 0, })), } } async function submit() { if (!imageFile) { onSave(buildBody()) return } if (!isNew) { onSave(buildBody()) setUploading(true) try { const fd = new FormData(); fd.append('file', imageFile) await client.post(`/api/products/${product.id}/image`, fd) qc.invalidateQueries({ queryKey: ['products-all'] }) } catch { toast.error('Σφάλμα ανεβάσματος εικόνας') } finally { setUploading(false) } } else { setUploading(true) try { const res = await client.post('/api/products/', buildBody()) const newId = res.data.id const fd = new FormData(); fd.append('file', imageFile) await client.post(`/api/products/${newId}/image`, fd) qc.invalidateQueries({ queryKey: ['products-all'] }) onClose() } catch { toast.error('Σφάλμα αποθήκευσης') } finally { setUploading(false) } } } const isNew = !product.id const canSave = form.name.trim() && form.base_price const favCount = buildFavoritesList(form).length const tabs = [ { key: 'favorites', label: 'Αγαπημένα', count: favCount, isFavTab: true }, { key: 'quick', label: 'Γρήγορες', count: form.quick_options.length }, { key: 'ingredients', label: 'Υλικά', count: form.ingredients.length }, { key: 'options', label: 'Έξτρα', count: form.options.length }, ...form.preference_sets.map((ps, i) => ({ key: i, label: ps.name || `Προτ. ${i + 1}`, count: ps.choices.length })), { key: '__add_pref__', label: '+ Προτίμηση', isAdd: true }, ] const favList = buildFavoritesList(form) return (
{/* ── Header ── */}

{isNew ? 'Νέο προϊόν' : `Επεξεργασία — ${product.name}`}

{/* ── Body: left/right split ── */}
{/* LEFT: product info */}

Στοιχεία προϊόντος

setField('name', e.target.value)} autoFocus placeholder="π.χ. Espresso" />
setField('base_price', v)} className="w-full" />
{/* Availability toggle */} {/* Image upload */}
{product.image_url && ( )} {imageFile && (
{imageFile.name}
)} {isNew && imageFile && (

Η εικόνα θα ανέβει μαζί με την αποθήκευση.

)}
{/* RIGHT: tabs */}
{/* Tab bar */}
{tabs.map(tab => { if (tab.isAdd) return ( ) const isActive = activeTab === tab.key return ( ) })}
{/* Tab content */}
{/* ── Favorites tab ── */} {activeTab === 'favorites' && (

Αγαπημένα — εμφανίζονται πρώτα στον σερβιτόρο. Σημειώστε ως αγαπημένο οποιοδήποτε στοιχείο από τις άλλες καρτέλες. Εδώ μπορείτε να τα αναδιατάξετε.

{favList.length === 0 && (

Δεν υπάρχουν αγαπημένα. Χρησιμοποιήστε το σε γρήγορες επιλογές, υλικά, έξτρα ή προτιμήσεις.

)}
{favList.map((fav, fi) => { const label = getItemLabel(form, fav.type, fav.idx) const typeLabel = getItemTypeLabel(fav.type) return (
moveFavorite(favList, fi, -1)} onDown={() => moveFavorite(favList, fi, 1)} disableUp={fi === 0} disableDown={fi === favList.length - 1} />

{label}

{typeLabel}

) })}
)} {/* ── Quick Options tab ── */} {activeTab === 'quick' && (

Γρήγορες επιλογές — χωρίς υπο-επιλογές.

{!form.quick_options.length &&

Δεν υπάρχουν γρήγορες επιλογές.

}
{form.quick_options.map((q, i) => (
moveQuickOption(i, -1)} onDown={() => moveQuickOption(i, 1)} disableUp={i === 0} disableDown={i === form.quick_options.length - 1} /> toggleFavorite('quick', i)} /> setQuickOption(i, 'name', e.target.value)} /> setQuickOption(i, 'price', v)} className="w-32" placeholder="0.00" />
))}
)} {/* ── Ingredients tab ── */} {activeTab === 'ingredients' && (

Υλικά που ο πελάτης μπορεί να αφαιρέσει.

{!form.ingredients.length &&

Δεν υπάρχουν υλικά.

}
{form.ingredients.map((ing, i) => (
moveIngredient(i, -1)} onDown={() => moveIngredient(i, 1)} disableUp={i === 0} disableDown={i === form.ingredients.length - 1} /> toggleFavorite('ingredient', i)} /> setIngredient(i, 'name', e.target.value)} /> setIngredient(i, 'extra_cost', v)} allowNegative className="w-32" />
))}
)} {/* ── Extras tab ── */} {activeTab === 'options' && (

Έξτρα (checkbox). Κάθε extra μπορεί να έχει υπο-επιλογές.

{!form.options.length &&

Δεν υπάρχουν extras.

}
{form.options.map((opt, i) => (
moveOption(i, -1)} onDown={() => moveOption(i, 1)} disableUp={i === 0} disableDown={i === form.options.length - 1} /> toggleFavorite('option', i)} /> setOption(i, 'name', e.target.value)} /> setOption(i, 'extra_cost', v)} allowNegative className="w-32" />
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 /> toggleFavorite('pref', si)} />

● = προεπιλογή (κλικ ξανά για αποεπιλογή) · ⊘ = απενεργοποιεί το κοινό υπο-σύνολο

{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)} />
))}

Κοινό υπο-σύνολο

Εμφανίζεται για όλες τις επιλογές εκτός αυτών με ⊘

{!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 && ( )}
) }