Files
simple-pos-system/manager_dashboard/src/pages/ProductsTab.jsx

1557 lines
82 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className="flex flex-wrap gap-2 mt-1">
{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>
)
}
function IconBase({ className = '', viewBox, strokeWidth = '1.5', children }) {
return (
<svg
aria-hidden="true"
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`inline-block shrink-0 ${className}`}
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
>
{children}
</svg>
)
}
function AddIcon({ className = '' }) {
return (
<IconBase className={className} viewBox="0 0 24 24">
<path d="M15 12H9" />
<path d="M12 9V15" />
<path d="M22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C21.5093 4.43821 21.8356 5.80655 21.9449 8" />
</IconBase>
)
}
function MoveUpIcon({ className = '' }) {
return (
<IconBase className={className} viewBox="-0.5 0 25 25">
<path d="M12 22.4199C17.5228 22.4199 22 17.9428 22 12.4199C22 6.89707 17.5228 2.41992 12 2.41992C6.47715 2.41992 2 6.89707 2 12.4199C2 17.9428 6.47715 22.4199 12 22.4199Z" />
<path d="M8 13.8599L10.87 10.8C11.0125 10.6416 11.1868 10.5149 11.3815 10.4282C11.5761 10.3415 11.7869 10.2966 12 10.2966C12.2131 10.2966 12.4239 10.3415 12.6185 10.4282C12.8132 10.5149 12.9875 10.6416 13.13 10.8L16 13.8599" />
</IconBase>
)
}
function MoveDownIcon({ className = '' }) {
return (
<IconBase className={className} viewBox="-0.5 0 25 25">
<path d="M12 22.4199C17.5228 22.4199 22 17.9428 22 12.4199C22 6.89707 17.5228 2.41992 12 2.41992C6.47715 2.41992 2 6.89707 2 12.4199C2 17.9428 6.47715 22.4199 12 22.4199Z" />
<path d="M16 10.99L13.13 14.05C12.9858 14.2058 12.811 14.3298 12.6166 14.4148C12.4221 14.4998 12.2122 14.5437 12 14.5437C11.7878 14.5437 11.5779 14.4998 11.3834 14.4148C11.189 14.3298 11.0142 14.2058 10.87 14.05L8 10.99" />
</IconBase>
)
}
function EditIcon({ className = '' }) {
return (
<IconBase className={className} viewBox="0 0 24 24">
<path d="M18 10L14 6M18 10L21 7L17 3L14 6M18 10L17 11M14 6L8 12V16H12L14.5 13.5M20 14V20H12M10 4L4 4L4 20H7" />
</IconBase>
)
}
function DeleteIcon({ className = '' }) {
return (
<IconBase className={className} viewBox="0 0 24 24" strokeWidth="2">
<path d="M10 11V17" />
<path d="M14 11V17" />
<path d="M4 7H20" />
<path d="M6 7H12H18V18C18 19.6569 16.6569 21 15 21H9C7.34315 21 6 19.6569 6 18V7Z" />
<path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" />
</IconBase>
)
}
function HeartIcon({ filled, className = '' }) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" className={`inline-block shrink-0 ${className}`}
fill={filled ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.8"
strokeLinecap="round" strokeLinejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
)
}
function ReorderBtns({ onUp, onDown, disableUp, disableDown }) {
return (
<div className="flex flex-col shrink-0">
<button type="button" onClick={onUp} disabled={disableUp}
className="text-gray-400 hover:text-gray-600 disabled:opacity-20 leading-none px-1 py-0.5 text-xs"></button>
<button type="button" onClick={onDown} disabled={disableDown}
className="text-gray-400 hover:text-gray-600 disabled:opacity-20 leading-none px-1 py-0.5 text-xs"></button>
</div>
)
}
function DefaultBtn({ isDefault, onClick, title }) {
return (
<button
type="button"
onClick={onClick}
title={title ?? (isDefault ? 'Κλικ για αποεπιλογή' : 'Ορισμός ως προεπιλογή')}
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 transition-all ${
isDefault
? 'border-primary-600 bg-primary-600'
: 'border-gray-300 bg-white hover:border-primary-400'
}`}
>
{isDefault && <span className="w-2.5 h-2.5 rounded-full bg-white block" />}
</button>
)
}
function FavoriteBtn({ isFavorite, onClick, title }) {
return (
<button
type="button"
onClick={onClick}
title={title ?? (isFavorite ? 'Αφαίρεση από αγαπημένα' : 'Προσθήκη στα αγαπημένα')}
className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 transition-all ${
isFavorite
? 'text-rose-500 hover:text-rose-400'
: 'text-gray-300 hover:text-rose-400'
}`}
>
<HeartIcon filled={isFavorite} className="w-4 h-4" />
</button>
)
}
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 (
<div className={`flex items-center border border-gray-300 rounded-lg overflow-hidden h-10 ${className}`}>
<button type="button" onClick={dec}
className="px-2 h-full text-gray-500 hover:bg-gray-100 border-r border-gray-300 text-sm font-bold shrink-0"></button>
<input
type="number" step="0.10" value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder ?? '0.00'}
className="flex-1 min-w-0 text-center text-sm outline-none bg-transparent px-1"
/>
<button type="button" onClick={inc}
className="px-2 h-full text-gray-500 hover:bg-gray-100 border-l border-gray-300 text-sm font-bold shrink-0">+</button>
</div>
)
}
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 (
<div className="border-t border-gray-100 bg-indigo-50/40 px-3 py-2 space-y-2">
<p className="text-xs text-indigo-500 font-medium mb-1">
Υπο-επιλογές του «{parentLabel || '…'}» εμφανίζονται μόνο αν επιλεγεί
</p>
{subChoices.map((sc, sci) => (
<div key={sci} className="flex items-center gap-2 ml-4">
<ReorderBtns
onUp={() => onMove(sci, -1)} onDown={() => onMove(sci, 1)}
disableUp={sci === 0} disableDown={sci === subChoices.length - 1}
/>
<DefaultBtn isDefault={sc.is_default} onClick={() => onToggleDefault(sci)} />
<input className="input flex-1 text-sm" placeholder="π.χ. Καραμέλα"
value={sc.name} onChange={e => onChange(sci, 'name', e.target.value)} />
<PriceInput value={sc.extra_cost} onChange={v => onChange(sci, 'extra_cost', v)}
allowNegative className="w-28 text-sm" />
<button onClick={() => onRemove(sci)} className="btn btn-danger px-2 min-h-0 h-9 text-sm shrink-0"></button>
</div>
))}
<button onClick={onAdd} className="ml-4 btn btn-secondary text-xs px-2 py-1 min-h-0 h-7">
+ Υπο-επιλογή
</button>
</div>
)
}
// 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_<id>' → 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 (
<div className="flex gap-6 h-full">
{/* Left: Categories */}
<aside className="w-96 shrink-0 space-y-1 overflow-y-auto">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-gray-700">Κατηγορίες</h2>
<button
onClick={() => setEditCat({ _isNew: true })}
className="btn btn-primary px-2 py-1 min-h-0 h-8 text-white"
title="Προσθήκη κατηγορίας"
aria-label="Προσθήκη κατηγορίας"
>
<AddIcon className={categoryIconClass} />
</button>
</div>
<button onClick={() => setSelectedCat(null)}
className={`w-full text-left px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${!selectedCat ? 'bg-primary-700 text-white' : 'hover:bg-gray-100 text-gray-700'}`}>
Όλα
</button>
{topLevelCats.map((cat, idx) => {
const subs = categories.filter(c => c.parent_id === cat.id).sort((a, b) => a.sort_order - b.sort_order)
const subList = buildSubList(cat, subs)
const isParentActive = selectedCat === cat.id
return (
<div key={cat.id}>
{/* Top-level category row */}
<div className={`flex items-center gap-1 rounded-xl ${isParentActive ? 'bg-primary-700 text-white' : 'hover:bg-gray-100'}`}>
{cat.color && <span className="w-2.5 h-2.5 rounded-full ml-2 shrink-0" style={{ background: cat.color }} />}
<button onClick={() => setSelectedCat(cat.id)} className="flex-1 text-left px-2 py-2.5 text-sm font-medium truncate">{cat.name}</button>
{/* Add sub-category button */}
<button
onClick={() => setEditCat({ _isNew: true, parent_id: cat.id })}
className={`${categoryActionBtnClass} text-green-600 hover:text-green-700`}
title="Προσθήκη υποκατηγορίας"
>
<AddIcon className={categoryIconClass} />
</button>
<button onClick={() => moveCat(cat, -1)} disabled={idx === 0} className={categoryMoveBtnClass} title="Μετακίνηση πάνω">
<MoveUpIcon className={categoryIconClass} />
</button>
<button onClick={() => moveCat(cat, 1)} disabled={idx === topLevelCats.length - 1} className={categoryMoveBtnClass} title="Μετακίνηση κάτω">
<MoveDownIcon className={categoryIconClass} />
</button>
<button onClick={() => setEditCat(cat)} className={categoryEditBtnClass} title="Επεξεργασία">
<EditIcon className={categoryIconClass} />
</button>
<button onClick={() => setConfirmDelete({ type: 'category', id: cat.id })} className={`${categoryDeleteBtnClass} mr-1`} title="Διαγραφή">
<DeleteIcon className={categoryIconClass} />
</button>
</div>
{/* Sub-category rows (indented), interleaved with General group */}
{subList.map((row, subIdx) => {
if (row._isGeneral) {
// Virtual "General" row — products directly on the parent
const isGeneralActive = selectedCat === `__general_${cat.id}`
return (
<div key="__general__" className={`flex items-center gap-1 ml-4 rounded-xl ${isGeneralActive ? 'bg-primary-600 text-white' : 'hover:bg-gray-100'}`}>
<span className="w-1.5 h-1.5 rounded-full ml-2 shrink-0 bg-gray-300" />
<button
onClick={() => setSelectedCat(`__general_${cat.id}`)}
className={`flex-1 text-left px-2 py-2 text-xs italic truncate ${selectedCat === `__general_${cat.id}` ? 'font-semibold' : 'text-gray-400'}`}
>Γενικά</button>
<button onClick={() => moveGeneral(cat, -1)} disabled={subIdx === 0} className={`${categoryMoveBtnClass} opacity-60`} title="Μετακίνηση Γενικά πάνω">
<MoveUpIcon className={categoryIconClass} />
</button>
<button onClick={() => moveGeneral(cat, 1)} disabled={subIdx === subList.length - 1} className={`${categoryMoveBtnClass} opacity-60 mr-1`} title="Μετακίνηση Γενικά κάτω">
<MoveDownIcon className={categoryIconClass} />
</button>
</div>
)
}
const isSubActive = selectedCat === row.id
return (
<div key={row.id} className={`flex items-center gap-1 ml-4 rounded-xl ${isSubActive ? 'bg-primary-600 text-white' : 'hover:bg-gray-100'}`}>
{row.color
? <span className="w-1.5 h-1.5 rounded-full ml-2 shrink-0" style={{ background: row.color }} />
: <span className="w-1.5 h-1.5 rounded-full ml-2 shrink-0 bg-gray-400" />
}
<button onClick={() => setSelectedCat(row.id)} className="flex-1 text-left px-2 py-2 text-xs font-medium truncate">{row.name}</button>
{/* Auto-expanded toggle */}
<button
type="button"
onClick={() => toggleAutoExpanded.mutate({ id: row.id, auto_expanded: !row.auto_expanded })}
title={row.auto_expanded ? 'Auto-expanded: ON — κλικ για απενεργοποίηση' : 'Auto-expanded: OFF — κλικ για ενεργοποίηση'}
className={`px-1.5 py-0.5 rounded text-xs font-bold shrink-0 transition-colors ${
row.auto_expanded
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
: 'bg-gray-100 text-gray-400 hover:bg-gray-200'
}`}
>
</button>
<button onClick={() => moveSubcat(cat, row, -1)} disabled={subIdx === 0} className={categoryMoveBtnClass} title="Μετακίνηση πάνω">
<MoveUpIcon className={categoryIconClass} />
</button>
<button onClick={() => moveSubcat(cat, row, 1)} disabled={subIdx === subList.length - 1} className={categoryMoveBtnClass} title="Μετακίνηση κάτω">
<MoveDownIcon className={categoryIconClass} />
</button>
<button onClick={() => setEditCat(row)} className={categoryEditBtnClass} title="Επεξεργασία">
<EditIcon className={categoryIconClass} />
</button>
<button onClick={() => setConfirmDelete({ type: 'category', id: row.id })} className={`${categoryDeleteBtnClass} mr-1`} title="Διαγραφή">
<DeleteIcon className={categoryIconClass} />
</button>
</div>
)
})}
</div>
)
})}
</aside>
{/* Right: Products */}
<div className="flex-1 space-y-4 min-w-0">
<div className="flex items-center gap-3 flex-wrap">
<h2 className="font-semibold text-gray-700 flex-1">
{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}`
})()
: 'Προϊόντα'
}
</h2>
<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" />
Ανενεργά
</label>
<label className="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" checked={showArchived} onChange={e => setShowArchived(e.target.checked)} className="accent-primary-700" />
Αρχειοθετημένα
</label>
<select value={sortMode} onChange={e => setSortMode(e.target.value)} className="input text-sm py-1 h-9 min-h-0 w-auto pr-8">
<option value="custom">Σειρά: Προσαρμοσμένη</option>
<option value="name">Σειρά: Αλφαβητικά</option>
<option value="price">Σειρά: Τιμή</option>
</select>
{!multiSelect && <button onClick={() => setMultiSelect(true)} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">Πολλαπλή επιλογή</button>}
{multiSelect && (
<div className="flex gap-2 flex-wrap">
<span className="text-sm text-gray-500 self-center">{selected.size} επιλεγμένα</span>
<button onClick={selectAll} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-8">Όλα</button>
<button onClick={clearSelect} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-8">Καθαρισμός</button>
<button onClick={() => bulkAction('available')} disabled={!selected.size} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-8 text-green-600">Διαθέσιμο</button>
<button onClick={() => bulkAction('unavailable')} disabled={!selected.size} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-8 text-amber-600">Μη διαθέσιμο</button>
<button onClick={() => bulkAction('archive')} disabled={!selected.size} className="btn btn-danger text-xs px-2 py-1 min-h-0 h-8">Αρχειοθέτηση</button>
<button onClick={exitMultiSelect} className="btn btn-secondary text-xs px-2 py-1 min-h-0 h-8">Ακύρωση</button>
</div>
)}
{!multiSelect && <button onClick={() => setEditProduct({ ...EMPTY_PRODUCT })} className="btn btn-primary">+ Νέο προϊόν</button>}
</div>
{products.length === 0 && <p className="text-center text-gray-400 py-16">Δεν υπάρχουν προϊόντα.</p>}
<div className="space-y-3">
{products.map((p, idx) => {
const isArchived = p.lifecycle_status === 'archived'
return (
<div key={p.id}
onClick={multiSelect ? () => 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 && (
<input type="checkbox" checked={selected.has(p.id)} onChange={() => toggleSelect(p.id)}
onClick={e => e.stopPropagation()} className="w-4 h-4 accent-primary-700 shrink-0" />
)}
{p.image_url && (
<img src={`${import.meta.env.VITE_API_URL || ''}${p.image_url}`} alt={p.name}
className="w-12 h-12 rounded-lg object-cover shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="font-semibold text-gray-800 flex items-center gap-2">
{p.name}
{isArchived && (
<span className="text-xs bg-gray-200 text-gray-600 px-1.5 py-0.5 rounded font-normal">Αρχείο</span>
)}
{!isArchived && !p.is_available && (
<span className="text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-normal">Ανενεργό</span>
)}
</p>
<p className="text-sm text-gray-500">
{(() => {
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)}`}
</p>
</div>
{!multiSelect && (
<>
{/* Availability toggle — only for non-archived products */}
{!isArchived && (
<button
onClick={e => { e.stopPropagation(); toggleAvail.mutate({ id: p.id, is_available: !p.is_available }) }}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-medium transition-colors shrink-0 ${
p.is_available
? 'bg-green-50 border-green-300 text-green-700 hover:bg-green-100'
: 'bg-gray-100 border-gray-300 text-gray-500 hover:bg-gray-200'
}`}
>
<span className={`w-2 h-2 rounded-full ${p.is_available ? 'bg-green-500' : 'bg-gray-400'}`} />
{p.is_available ? 'Διαθέσιμο' : 'Ανενεργό'}
</button>
)}
<button onClick={() => setEditProduct(p)} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9 shrink-0">Επεξεργασία</button>
{/* Archive/delete button — context-sensitive */}
{isArchived ? (
<button
onClick={() => setConfirmDelete({ type: 'product-hard', id: p.id })}
className="btn btn-danger text-sm px-3 py-1.5 min-h-0 h-9 shrink-0"
title="Οριστική διαγραφή"
>
Διαγραφή
</button>
) : (
<button
onClick={() => setConfirmDelete({ type: 'product-archive', id: p.id })}
className="btn btn-danger text-sm px-3 py-1.5 min-h-0 h-9 shrink-0"
title="Αρχειοθέτηση (διατηρείται για ιστορικό)"
>
Αρχειοθέτηση
</button>
)}
{sortMode === 'custom' && !isArchived && (
<div className="flex flex-col gap-0.5 shrink-0">
<button onClick={() => moveProd(p, -1)} disabled={idx === 0} className="text-gray-400 hover:text-gray-600 disabled:opacity-20 text-xs leading-none"></button>
<button onClick={() => moveProd(p, 1)} disabled={idx === products.length - 1} className="text-gray-400 hover:text-gray-600 disabled:opacity-20 text-xs leading-none"></button>
</div>
)}
</>
)}
</div>
)
})}
</div>
</div>
{editCat !== null && (
<CategoryFormModal
cat={editCat}
parentName={editCat.parent_id ? categories.find(c => 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 && (
<ProductFormModal
product={editProduct} categories={categories} printers={printers}
onSave={b => saveProduct.mutate(b)}
onCopy={formData => setEditProduct({ ...EMPTY_PRODUCT, ...formData, id: undefined, image_url: undefined, name: formData.name + ' (αντίγραφο)' })}
onClose={() => setEditProduct(null)}
/>
)}
{confirmDelete && (
<ConfirmModal
title="Επιβεβαίωση"
message={
confirmDelete.type === 'category'
? 'Η κατηγορία θα διαγραφεί.'
: confirmDelete.type === 'bulk-archive'
? `${confirmDelete.ids.length} προϊόντα θα αρχειοθετηθούν (αν υπάρχουν σε παραγγελίες) ή θα διαγραφούν.`
: confirmDelete.type === 'product-archive'
? 'Το προϊόν θα αρχειοθετηθεί. Αν δεν υπάρχει σε παλαιές παραγγελίες θα διαγραφεί οριστικά, αλλιώς θα κρατηθεί για ιστορικό.'
: 'Οριστική διαγραφή. Το προϊόν θα χαθεί μόνιμα.'
}
confirmLabel={confirmDelete.type === 'category' ? 'Επιβεβαίωση' : confirmDelete.type === 'product-hard' ? 'Διαγραφή' : 'Αρχειοθέτηση'}
confirmClass="btn-danger"
onConfirm={handleConfirmDelete}
onCancel={() => setConfirmDelete(null)}
/>
)}
</div>
)
}
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 (
<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">
<h2 className="font-bold text-gray-800">{title}</h2>
{isSub && parentName && (
<p className="text-xs text-gray-400 -mt-2">Κατηγορία: <span className="font-medium text-gray-600">{parentName}</span></p>
)}
<div>
<label className="label">Όνομα</label>
<input className="input" value={name} onChange={e => setName(e.target.value)} autoFocus />
</div>
<div>
<label className="label">Χρώμα</label>
<ColorPicker value={color} onChange={setColor} />
</div>
<div className="flex gap-3 pt-2">
<button onClick={onClose} className="flex-1 btn btn-secondary">Ακύρωση</button>
<button onClick={() => onSave(name, color || null)} disabled={!name.trim()} className="flex-1 btn btn-primary">Αποθήκευση</button>
</div>
</div>
</div>
)
}
// ─── 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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden" style={{ width: '90vw', height: '92vh' }}>
{/* ── Header ── */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 shrink-0">
<h2 className="font-bold text-gray-800 text-lg">
{isNew ? 'Νέο προϊόν' : `Επεξεργασία — ${product.name}`}
</h2>
<button onClick={onClose} className="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center text-gray-500 text-lg"></button>
</div>
{/* ── Body: left/right split ── */}
<div className="flex-1 flex overflow-hidden">
{/* LEFT: product info */}
<div className="w-80 shrink-0 border-r border-gray-100 bg-gray-50/50 px-5 py-5 flex flex-col gap-3">
<p className="text-xs font-semibold text-gray-400 uppercase tracking-widest">Στοιχεία προϊόντος</p>
<div>
<label className="label">Όνομα *</label>
<input className="input" value={form.name} onChange={e => setField('name', e.target.value)} autoFocus placeholder="π.χ. Espresso" />
</div>
<div>
<label className="label">Τιμή βάσης () *</label>
<PriceInput value={form.base_price} onChange={v => setField('base_price', v)} className="w-full" />
</div>
<div>
<label className="label">Κατηγορία</label>
<select className="input" value={form.category_id} onChange={e => setField('category_id', e.target.value)}>
<option value=""> Χωρίς κατηγορία </option>
{categories.filter(c => !c.parent_id).sort((a, b) => a.sort_order - b.sort_order).flatMap(parent => {
const subs = categories.filter(c => c.parent_id === parent.id).sort((a, b) => a.sort_order - b.sort_order)
return [
<option key={parent.id} value={parent.id}>{parent.name}</option>,
...subs.map(s => <option key={s.id} value={s.id}>&nbsp;&nbsp; {s.name}</option>),
]
})}
</select>
</div>
<div>
<label className="label">Ζώνη εκτυπωτή</label>
<select className="input" value={form.printer_zone_id} onChange={e => setField('printer_zone_id', e.target.value)}>
<option value=""> Χωρίς εκτυπωτή </option>
{printers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
{/* Availability toggle */}
<button
type="button"
onClick={() => setField('is_available', !form.is_available)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-colors ${
form.is_available
? 'bg-green-50 border-green-300 text-green-700 hover:bg-green-100'
: 'bg-gray-100 border-gray-300 text-gray-500 hover:bg-gray-200'
}`}
>
<span className={`w-2.5 h-2.5 rounded-full ${form.is_available ? 'bg-green-500' : 'bg-gray-400'}`} />
{form.is_available ? 'Διαθέσιμο' : 'Μη διαθέσιμο'}
</button>
{/* Image upload */}
<div>
<label className="label">Εικόνα προϊόντος</label>
{product.image_url && (
<img src={`${import.meta.env.VITE_API_URL || ''}${product.image_url}`}
className="w-16 h-16 rounded-xl object-cover border border-gray-200 mb-2" alt="" />
)}
{imageFile && (
<div className="text-xs text-primary-700 font-medium mb-1 break-all">{imageFile.name}</div>
)}
<label className="cursor-pointer inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 text-sm text-gray-600 transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
{imageFile ? 'Αλλαγή εικόνας' : 'Επιλογή εικόνας'}
<input type="file" accept="image/*" className="sr-only"
onChange={e => setImageFile(e.target.files[0] ?? null)} />
</label>
{isNew && imageFile && (
<p className="text-xs text-gray-400 mt-1">Η εικόνα θα ανέβει μαζί με την αποθήκευση.</p>
)}
</div>
</div>
{/* RIGHT: tabs */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Tab bar */}
<div className="flex border-b border-gray-200 overflow-x-auto shrink-0 bg-white">
{tabs.map(tab => {
if (tab.isAdd) return (
<button key="__add_pref__" onClick={addPrefSet}
className="px-4 py-3 text-sm font-medium text-primary-600 hover:bg-primary-50 whitespace-nowrap border-b-2 border-transparent transition-colors">
{tab.label}
</button>
)
const isActive = activeTab === tab.key
return (
<button key={String(tab.key)} onClick={() => setActiveTab(tab.key)}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors flex items-center gap-1.5 ${
isActive ? 'border-primary-600 text-primary-700 bg-primary-50/50' : 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`}>
{tab.isFavTab && <HeartIcon filled={favCount > 0} className={`w-3.5 h-3.5 ${favCount > 0 ? 'text-rose-500' : 'text-gray-400'}`} />}
{tab.label}
{tab.count > 0 && (
<span className={`text-xs px-1.5 py-0.5 rounded-full font-mono ${isActive ? 'bg-primary-100 text-primary-700' : 'bg-gray-100 text-gray-500'}`}>
{tab.count}
</span>
)}
</button>
)
})}
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto px-6 py-5">
{/* ── Favorites tab ── */}
{activeTab === 'favorites' && (
<div>
<p className="text-sm text-gray-500 mb-4">
Αγαπημένα εμφανίζονται πρώτα στον σερβιτόρο. Σημειώστε ως αγαπημένο οποιοδήποτε στοιχείο από τις άλλες καρτέλες. Εδώ μπορείτε να τα αναδιατάξετε.
</p>
{favList.length === 0 && (
<p className="text-sm text-gray-400 text-center py-12">
Δεν υπάρχουν αγαπημένα. Χρησιμοποιήστε το <HeartIcon className="w-4 h-4 inline text-gray-400" /> σε γρήγορες επιλογές, υλικά, έξτρα ή προτιμήσεις.
</p>
)}
<div className="space-y-2">
{favList.map((fav, fi) => {
const label = getItemLabel(form, fav.type, fav.idx)
const typeLabel = getItemTypeLabel(fav.type)
return (
<div key={`${fav.type}-${fav.idx}`} className="flex items-center gap-3 border border-rose-100 bg-rose-50/30 rounded-xl p-3">
<ReorderBtns
onUp={() => moveFavorite(favList, fi, -1)}
onDown={() => moveFavorite(favList, fi, 1)}
disableUp={fi === 0}
disableDown={fi === favList.length - 1}
/>
<HeartIcon filled className="w-4 h-4 text-rose-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">{label}</p>
<p className="text-xs text-gray-400">{typeLabel}</p>
</div>
<button
type="button"
onClick={() => toggleFavorite(fav.type, fav.idx)}
className="text-xs text-gray-400 hover:text-red-500 transition-colors px-2 py-1 rounded hover:bg-red-50"
>
Αφαίρεση
</button>
</div>
)
})}
</div>
</div>
)}
{/* ── Quick Options tab ── */}
{activeTab === 'quick' && (
<div>
<div className="flex items-center justify-between mb-3">
<p className="text-sm text-gray-500">Γρήγορες επιλογές χωρίς υπο-επιλογές.</p>
<button onClick={addQuickOption} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">+ Επιλογή</button>
</div>
{!form.quick_options.length && <p className="text-sm text-gray-400 text-center py-8">Δεν υπάρχουν γρήγορες επιλογές.</p>}
<div className="space-y-2">
{form.quick_options.map((q, i) => (
<div key={i} className={`flex gap-2 items-center border rounded-xl p-3 bg-white ${q.is_favorite ? 'border-rose-200' : 'border-gray-200'}`}>
<ReorderBtns onUp={() => moveQuickOption(i, -1)} onDown={() => moveQuickOption(i, 1)}
disableUp={i === 0} disableDown={i === form.quick_options.length - 1} />
<FavoriteBtn isFavorite={q.is_favorite} onClick={() => toggleFavorite('quick', i)} />
<input className="input flex-1" placeholder="Όνομα (π.χ. Extra Bacon)"
value={q.name} onChange={e => setQuickOption(i, 'name', e.target.value)} />
<PriceInput value={q.price} onChange={v => setQuickOption(i, 'price', v)}
className="w-32" placeholder="0.00" />
<label className="flex items-center gap-1.5 text-sm text-gray-600 cursor-pointer shrink-0 select-none">
<input type="checkbox" checked={q.allow_multiple}
onChange={e => setQuickOption(i, 'allow_multiple', e.target.checked)}
className="accent-primary-700 w-4 h-4" />
Πολλαπλά
</label>
<button onClick={() => removeQuickOption(i)} className="btn btn-danger px-3 min-h-0 h-10"></button>
</div>
))}
</div>
</div>
)}
{/* ── Ingredients tab ── */}
{activeTab === 'ingredients' && (
<div>
<div className="flex items-center justify-between mb-3">
<p className="text-sm text-gray-500">Υλικά που ο πελάτης μπορεί να αφαιρέσει.</p>
<button onClick={addIngredient} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">+ Υλικό</button>
</div>
{!form.ingredients.length && <p className="text-sm text-gray-400 text-center py-8">Δεν υπάρχουν υλικά.</p>}
<div className="space-y-2">
{form.ingredients.map((ing, i) => (
<div key={i} className={`flex gap-2 items-center border rounded-xl p-3 bg-white ${ing.is_favorite ? 'border-rose-200' : 'border-gray-200'}`}>
<ReorderBtns onUp={() => moveIngredient(i, -1)} onDown={() => moveIngredient(i, 1)}
disableUp={i === 0} disableDown={i === form.ingredients.length - 1} />
<FavoriteBtn isFavorite={ing.is_favorite} onClick={() => toggleFavorite('ingredient', i)} />
<input className="input flex-1" placeholder="Όνομα υλικού" value={ing.name}
onChange={e => setIngredient(i, 'name', e.target.value)} />
<PriceInput value={ing.extra_cost} onChange={v => setIngredient(i, 'extra_cost', v)}
allowNegative className="w-32" />
<button onClick={() => removeIngredient(i)} className="btn btn-danger px-3 min-h-0 h-10"></button>
</div>
))}
</div>
</div>
)}
{/* ── Extras tab ── */}
{activeTab === 'options' && (
<div>
<div className="flex items-center justify-between mb-3">
<p className="text-sm text-gray-500">Έξτρα (checkbox). Κάθε extra μπορεί να έχει υπο-επιλογές.</p>
<button onClick={addOption} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">+ Έξτρα</button>
</div>
{!form.options.length && <p className="text-sm text-gray-400 text-center py-8">Δεν υπάρχουν extras.</p>}
<div className="space-y-3">
{form.options.map((opt, i) => (
<div key={i} className={`border rounded-xl overflow-hidden ${opt.is_favorite ? 'border-rose-200' : 'border-gray-200'}`}>
<div className="flex gap-2 items-center p-3 bg-white flex-wrap">
<ReorderBtns onUp={() => moveOption(i, -1)} onDown={() => moveOption(i, 1)}
disableUp={i === 0} disableDown={i === form.options.length - 1} />
<FavoriteBtn isFavorite={opt.is_favorite} onClick={() => toggleFavorite('option', i)} />
<input className="input flex-1 min-w-40" placeholder="Όνομα extra (π.χ. Κανέλα)"
value={opt.name} onChange={e => setOption(i, 'name', e.target.value)} />
<PriceInput value={opt.extra_cost} onChange={v => setOption(i, 'extra_cost', v)}
allowNegative className="w-32" />
<label className="flex items-center gap-1.5 text-sm text-gray-600 cursor-pointer shrink-0 select-none">
<input type="checkbox" checked={opt.allow_multiple}
onChange={e => setOption(i, 'allow_multiple', e.target.checked)}
className="accent-primary-700 w-4 h-4" />
Πολλαπλά
</label>
<button onClick={() => addOptionSubChoice(i)}
className="btn btn-secondary text-xs px-2 min-h-0 h-9 shrink-0 whitespace-nowrap">
+ Υπο-επιλογές
</button>
<button onClick={() => removeOption(i)} className="btn btn-danger px-3 min-h-0 h-10"></button>
</div>
<SubChoiceRows
subChoices={opt.sub_choices}
parentLabel={opt.name}
onMove={(sci, dir) => 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)}
/>
</div>
))}
</div>
</div>
)}
{/* ── 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 (
<div>
<div className="flex items-center gap-3 mb-4">
<input className="input flex-1 font-semibold text-base"
placeholder="Όνομα προτίμησης (π.χ. Ζάχαρη)" value={ps.name}
onChange={e => setPrefSetField(si, 'name', e.target.value)} autoFocus />
<FavoriteBtn isFavorite={ps.is_favorite} onClick={() => toggleFavorite('pref', si)} />
<button onClick={() => removePrefSet(si)} className="btn btn-danger px-3 min-h-0 h-10 shrink-0">Διαγραφή</button>
</div>
<p className="text-xs text-gray-400 mb-3">
= προεπιλογή (κλικ ξανά για αποεπιλογή) · = απενεργοποιεί το κοινό υπο-σύνολο
</p>
<div className="space-y-3 mb-5">
{ps.choices.map((ch, ci) => (
<div key={ci} className="border border-gray-200 rounded-xl overflow-hidden">
<div className="flex items-center gap-2 p-3 bg-white">
<ReorderBtns onUp={() => moveChoice(si, ci, -1)} onDown={() => moveChoice(si, ci, 1)}
disableUp={ci === 0} disableDown={ci === ps.choices.length - 1} />
<DefaultBtn isDefault={ps.default_choice_index === ci} onClick={() => toggleDefaultChoice(si, ci)} />
<input className="input flex-1" placeholder="Όνομα επιλογής (π.χ. Σκέτος)"
value={ch.name} onChange={e => setChoice(si, ci, 'name', e.target.value)} />
<PriceInput value={ch.extra_cost} onChange={v => setChoice(si, ci, 'extra_cost', v)}
allowNegative className="w-32" />
{hasSharedSubset && (
<button type="button"
onClick={() => setChoice(si, ci, 'disables_subset', !ch.disables_subset)}
title={ch.disables_subset ? 'Εμφάνιση κοινού υπο-συνόλου' : 'Απόκρυψη κοινού υπο-συνόλου'}
className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 text-sm transition-colors ${
ch.disables_subset ? 'bg-red-100 text-red-500' : 'text-gray-300 hover:text-red-400'
}`}></button>
)}
<button onClick={() => addSubChoice(si, ci)}
className="btn btn-secondary text-xs px-2 min-h-0 h-9 shrink-0 whitespace-nowrap">
+ Υπο-επιλογές
</button>
<button onClick={() => removeChoice(si, ci)} className="btn btn-danger px-2 min-h-0 h-9 shrink-0"></button>
</div>
<SubChoiceRows
subChoices={ch.sub_choices}
parentLabel={ch.name}
onMove={(sci, dir) => 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)}
/>
</div>
))}
</div>
<button onClick={() => addChoice(si)} className="btn btn-secondary text-sm px-4 py-1.5 min-h-0 h-9">
+ Επιλογή
</button>
<div className="mt-6 border-t border-gray-100 pt-5">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-sm font-semibold text-gray-700">Κοινό υπο-σύνολο</p>
<p className="text-xs text-gray-400">Εμφανίζεται για όλες τις επιλογές εκτός αυτών με </p>
</div>
{!ps.shared_subset ? (
<button onClick={() => setSharedSubsetName(si, '')} className="btn btn-secondary text-sm px-3 py-1.5 min-h-0 h-9">
+ Κοινό υπο-σύνολο
</button>
) : (
<button onClick={() => setPrefSetField(si, 'shared_subset', null)} className="btn btn-danger text-sm px-3 py-1.5 min-h-0 h-9">
Αφαίρεση
</button>
)}
</div>
{ps.shared_subset && (
<div className="border border-indigo-200 rounded-xl p-4 bg-indigo-50/30 space-y-3">
<div>
<label className="label text-xs">Όνομα (π.χ. Είδος ζάχαρης)</label>
<input className="input text-sm" placeholder="π.χ. Είδος ζάχαρης"
value={ps.shared_subset.name || ''}
onChange={e => setSharedSubsetName(si, e.target.value)} />
</div>
<div className="space-y-2">
{(ps.shared_subset.choices || []).map((sc, sci) => (
<div key={sci} className="flex items-center gap-2">
<ReorderBtns onUp={() => moveSharedSubsetChoice(si, sci, -1)} onDown={() => moveSharedSubsetChoice(si, sci, 1)}
disableUp={sci === 0} disableDown={sci === ps.shared_subset.choices.length - 1} />
<DefaultBtn isDefault={sc.is_default} onClick={() => setSharedSubsetChoice(si, sci, 'is_default', !sc.is_default)} />
<input className="input flex-1 text-sm" placeholder="π.χ. Λευκή"
value={sc.name} onChange={e => setSharedSubsetChoice(si, sci, 'name', e.target.value)} />
<PriceInput value={sc.extra_cost} onChange={v => setSharedSubsetChoice(si, sci, 'extra_cost', v)}
allowNegative className="w-32 text-sm" />
<button onClick={() => removeSharedSubsetChoice(si, sci)} className="btn btn-danger px-2 min-h-0 h-9 text-sm shrink-0"></button>
</div>
))}
</div>
<button onClick={() => addSharedSubsetChoice(si)} className="btn btn-secondary text-xs px-3 py-1 min-h-0 h-7">
+ Επιλογή υπο-συνόλου
</button>
</div>
)}
</div>
</div>
)
})()}
</div>
</div>
</div>
{/* ── Footer ── */}
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-100 bg-gray-50/50 shrink-0 gap-3">
<button onClick={onClose} className="btn btn-secondary px-6">Ακύρωση</button>
<div className="flex gap-3">
{!isNew && (
<button onClick={() => onCopy({ ...form })} className="btn btn-secondary px-6 text-indigo-600 border-indigo-200 hover:bg-indigo-50">
📋 Αντιγραφή
</button>
)}
<button onClick={submit} disabled={!canSave || uploading} className="btn btn-primary px-8">
{uploading ? 'Ανέβασμα…' : isNew ? 'Δημιουργία' : 'Αποθήκευση'}
</button>
</div>
</div>
</div>
</div>
)
}