import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Plus, Percent, Power, Pencil, Trash2, Star, Layers,
GripVertical, ChevronDown, ChevronUp, X, Info,
} from 'lucide-react'
import toast from 'react-hot-toast'
import client from '../../../api/client'
import Button from '../../../ui/Button'
import Modal from '../../../ui/Modal'
import { ConfirmModal } from '../../../ui/Modal'
// ─── API ─────────────────────────────────────────────────────────────────────
const api = {
list: (params) => client.get('/api/pricing/modifiers', { params }).then(r => r.data),
create: body => client.post('/api/pricing/modifiers', body).then(r => r.data),
update: (id, b) => client.put(`/api/pricing/modifiers/${id}`, b).then(r => r.data),
toggle: id => client.patch(`/api/pricing/modifiers/${id}/toggle`).then(r => r.data),
reorder: items => client.patch('/api/pricing/modifiers/reorder', { items }),
remove: id => client.delete(`/api/pricing/modifiers/${id}`),
groups: () => client.get('/api/pricing/groups').then(r => r.data),
products: () => client.get('/api/products', { params: { all: true } }).then(r => r.data),
categories: () => client.get('/api/products/categories').then(r => r.data),
prepZones: () => client.get('/api/prep-zones').then(r => r.data),
}
// ─── Constants ───────────────────────────────────────────────────────────────
const COLORS = [
'#6366f1','#8b5cf6','#ec4899','#f43f5e','#f97316',
'#eab308','#22c55e','#14b8a6','#0ea5e9','#64748b',
]
const CONDITION_TYPES = [
{ value: 'time_range', label: 'Ώρα ημέρας (εύρος)' },
{ value: 'date_range', label: 'Εύρος ημερομηνιών' },
{ value: 'specific_date', label: 'Συγκεκριμένη ημερομηνία' },
{ value: 'day_of_week', label: 'Ημέρα εβδομάδας' },
{ value: 'min_item_quantity', label: 'Ελάχιστη ποσότητα προϊόντος' },
{ value: 'min_category_qty', label: 'Ελάχιστη ποσότητα κατηγορίας' },
{ value: 'min_order_value', label: 'Ελάχιστη αξία παραγγελίας' },
{ value: 'order_channel', label: 'Κανάλι παραγγελίας' },
{ value: 'price_group_active', label: 'Ομάδα τιμής ενεργή' },
{ value: 'user_tier', label: 'Επίπεδο πελάτη (placeholder)' },
{ value: 'low_stock', label: 'Χαμηλό απόθεμα (placeholder)' },
]
const DAYS = ['Δευτέρα','Τρίτη','Τετάρτη','Πέμπτη','Παρασκευή','Σάββατο','Κυριακή']
const CHANNELS = ['pos','online','qr','takeaway']
const ROUND_OPTIONS = [
{ value: '', label: 'Χωρίς στρογγυλοποίηση' },
{ value: '0.05', label: 'Στο κοντινότερο €0,05' },
{ value: '0.10', label: 'Στο κοντινότερο €0,10' },
{ value: '0.20', label: 'Στο κοντινότερο €0,20' },
{ value: '0.50', label: 'Στο κοντινότερο €0,50' },
{ value: 'x.99', label: 'Σε x,99 (π.χ. 4,99)' },
{ value: 'x.00', label: 'Σε x,00 (στρογγυλό)' },
]
// ─── Condition editor ─────────────────────────────────────────────────────────
function ConditionEditor({ cond, onChange, onRemove, groups, categories, prepZones }) {
const set = (k, v) => onChange({ ...cond, params: { ...cond.params, [k]: v } })
const setType = t => onChange({ condition_type: t, params: {} })
return (
{/* Params per type */}
{cond.condition_type === 'time_range' && (
)}
{cond.condition_type === 'date_range' && (
)}
{cond.condition_type === 'specific_date' && (
)}
{cond.condition_type === 'day_of_week' && (
{DAYS.map((d, i) => (
))}
)}
{cond.condition_type === 'min_item_quantity' && (
set('min', Number(e.target.value))} />
)}
{cond.condition_type === 'min_category_qty' && (
)}
{cond.condition_type === 'min_order_value' && (
set('min', Number(e.target.value))} />
)}
{cond.condition_type === 'order_channel' && (
{CHANNELS.map(ch => (
))}
)}
{cond.condition_type === 'price_group_active' && (
)}
{(cond.condition_type === 'user_tier' || cond.condition_type === 'low_stock') && (
Placeholder — θα ενεργοποιηθεί σε μελλοντική έκδοση
)}
)
}
// ─── Shared multi-picker ──────────────────────────────────────────────────────
function MultiPicker({ items, selected, onToggle, labelKey = 'name', valueKey = 'id', placeholder = 'Επιλέξτε...' }) {
return (
{items.length === 0 && (
{placeholder}
)}
{items.map(item => {
const val = item[valueKey]
const checked = selected.includes(val)
return (
onToggle(val)}
className={`flex items-center gap-2.5 px-3 py-1.5 cursor-pointer transition-colors text-[12px] border-b border-slate-100 last:border-0 ${
checked ? 'bg-sky-50 text-sky-700' : 'hover:bg-slate-50 text-slate-700'
}`}>
{item[labelKey]}
)
})}
)
}
function TagPicker({ selected, onToggle, onAdd, allTags }) {
const [draft, setDraft] = useState('')
const handleAdd = () => {
const tag = draft.trim()
if (tag && !selected.includes(tag)) onAdd(tag)
setDraft('')
}
return (
{/* Quick-select existing tags */}
{allTags.length > 0 && (
{allTags.map(tag => {
const active = selected.includes(tag)
return (
)
})}
)}
{/* Selected chips */}
{selected.length > 0 && (
{selected.map(tag => (
{tag}
))}
)}
{/* Manual entry */}
setDraft(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAdd() } }} />
)
}
// ─── Target editor ────────────────────────────────────────────────────────────
function TargetEditor({ target, onChange, onRemove, products, categories, prepZones, allTags }) {
const ids = target.target_ids ?? []
const tags = target.target_tags ?? []
const setIds = v => onChange({ ...target, target_ids: v, target_id: v[0] ?? null })
const setTags = v => onChange({ ...target, target_tags: v, target_tag: v[0] ?? null })
const toggleId = id => setIds(ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id])
const toggleTag = tag => setTags(tags.includes(tag) ? tags.filter(x => x !== tag) : [...tags, tag])
return (
{target.target_type === 'item' && (
)}
{target.target_type === 'category' && (
)}
{target.target_type === 'prep_zone' && (
)}
{target.target_type === 'tag' && (
setTags([...tags, tag])} allTags={allTags} />
)}
)
}
// ─── Modifier form ────────────────────────────────────────────────────────────
const MODIFIER_TABS = [
{ key: 'info', label: 'Πληροφορίες' },
{ key: 'target', label: 'Στόχος' },
{ key: 'conditions', label: 'Συνθήκες' },
{ key: 'price', label: 'Τιμή' },
]
function ModifierForm({ initial, onSave, onCancel, saving, groups, activeTab, onTabChange }) {
const { data: products = [] } = useQuery({
queryKey: ['products-all-pricing'],
queryFn: () => client.get('/api/products/', { params: { all: true } }).then(r => r.data),
})
const { data: categories = [] } = useQuery({
queryKey: ['categories'],
queryFn: () => client.get('/api/products/categories').then(r => r.data),
staleTime: 60_000,
})
const { data: prepZones = [] } = useQuery({
queryKey: ['prep-zones'],
queryFn: () => client.get('/api/prep-zones').then(r => r.data),
staleTime: 60_000,
})
const { data: allTags = [] } = useQuery({
queryKey: ['product-tags'],
queryFn: () => client.get('/api/products/tags').then(r => r.data),
staleTime: 60_000,
})
const normalizeTarget = t => ({
...t,
target_ids: t.target_ids ?? (t.target_id != null ? [t.target_id] : []),
target_tags: t.target_tags ?? (t.target_tag ? [t.target_tag] : []),
})
const blank = {
name: '', description: '', color: COLORS[0], is_active: true, is_favorite: false,
allow_stack: false, sort_order: 0, scope: 'global', item_id: null,
action_type: 'add_percent', action_value: -10, round_to: '',
conditions: [], targets: [{ target_type: 'all', target_id: null, target_tag: null, target_ids: [], target_tags: [] }],
}
const [f, setF] = useState(initial ? {
...initial,
action_value: initial.action_value ?? 0,
round_to: initial.round_to ?? '',
conditions: initial.conditions ?? [],
targets: (initial.targets ?? []).map(normalizeTarget),
} : blank)
const set = (k, v) => setF(p => ({ ...p, [k]: v }))
const addCondition = () => set('conditions', [...f.conditions, { condition_type: 'time_range', params: {} }])
const updateCondition = (i, c) => set('conditions', f.conditions.map((x, j) => j === i ? c : x))
const removeCondition = i => set('conditions', f.conditions.filter((_, j) => j !== i))
const addTarget = () => set('targets', [...f.targets, { target_type: 'all', target_id: null, target_tag: null, target_ids: [], target_tags: [] }])
const updateTarget = (i, t) => set('targets', f.targets.map((x, j) => j === i ? t : x))
const removeTarget = i => set('targets', f.targets.filter((_, j) => j !== i))
function submit(e) {
e.preventDefault()
if (!f.name.trim()) return toast.error('Απαιτείται όνομα')
if (f.scope === 'item' && !f.item_id) return toast.error('Επιλέξτε προϊόν για item-scope')
onSave({ ...f, round_to: f.round_to || null })
}
const actionLabel = f.action_type === 'set' ? 'Νέα τιμή (€)' :
f.action_type === 'add_amount' ? 'Ποσό (€, αρνητικό = έκπτωση)' :
'Ποσοστό (%, αρνητικό = έκπτωση)'
return (
)
}
// ─── Modifier card ────────────────────────────────────────────────────────────
function ModifierCard({ modifier, onEdit, onDelete, dragHandleProps }) {
const qc = useQueryClient()
const [expanded, setExpanded] = useState(false)
const toggle = useMutation({
mutationFn: () => api.toggle(modifier.id),
onSuccess: () => qc.invalidateQueries({ queryKey: ['pricing-modifiers'] }),
onError: () => toast.error('Σφάλμα'),
})
const actionText = modifier.action_type === 'set'
? `= €${modifier.action_value}`
: modifier.action_type === 'add_amount'
? `${modifier.action_value >= 0 ? '+' : ''}€${modifier.action_value}`
: `${modifier.action_value >= 0 ? '+' : ''}${modifier.action_value}%`
const condCount = modifier.conditions?.length ?? 0
const targetSummary = modifier.scope === 'item'
? 'item-scope'
: modifier.targets?.length === 1 && modifier.targets[0]?.target_type === 'all'
? 'Όλα τα προϊόντα'
: `${modifier.targets?.length ?? 0} στόχοι`
return (
{/* Drag handle */}
{/* Color bar */}
{/* Name + badges */}
{modifier.name}
{modifier.is_favorite && }
{modifier.allow_stack && }
{actionText}
{condCount === 0 ? 'χειροκίνητος' : `${condCount} συνθ.`} · {targetSummary}
{modifier.description &&
{modifier.description}
}
{/* Actions */}
{/* Expanded detail */}
{expanded && (
{modifier.conditions?.length === 0 &&
Χωρίς αυτόματες συνθήκες — μόνο χειροκίνητη ενεργοποίηση
}
{modifier.conditions?.map((c, i) => (
{CONDITION_TYPES.find(x => x.value === c.condition_type)?.label ?? c.condition_type}
{JSON.stringify(c.params)}
))}
)}
)
}
// ─── Main tab ─────────────────────────────────────────────────────────────────
export default function ModifiersTab() {
const qc = useQueryClient()
const [showForm, setShowForm] = useState(false)
const [editing, setEditing] = useState(null)
const [deleting, setDeleting] = useState(null)
const [formTab, setFormTab] = useState('info')
const [scopeFilter, setScopeFilter] = useState('all')
const [items, setItems] = useState(null)
const [dragging, setDragging] = useState(null)
const { data: rawModifiers = [], isLoading } = useQuery({
queryKey: ['pricing-modifiers'],
queryFn: () => api.list(),
})
const modifiers = items ?? rawModifiers
const { data: groups = [] } = useQuery({ queryKey: ['pricing-groups'], queryFn: api.groups, staleTime: 30_000 })
const create = useMutation({
mutationFn: api.create,
onSuccess: () => { qc.invalidateQueries({ queryKey: ['pricing-modifiers'] }); setItems(null); setShowForm(false); toast.success('Δημιουργήθηκε') },
onError: () => toast.error('Σφάλμα'),
})
const update = useMutation({
mutationFn: ({ id, body }) => api.update(id, body),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['pricing-modifiers'] }); setItems(null); setEditing(null); toast.success('Αποθηκεύτηκε') },
onError: () => toast.error('Σφάλμα'),
})
const remove = useMutation({
mutationFn: api.remove,
onSuccess: () => { qc.invalidateQueries({ queryKey: ['pricing-modifiers'] }); setItems(null); setDeleting(null); toast.success('Διαγράφηκε') },
onError: () => toast.error('Σφάλμα'),
})
const reorder = useMutation({
mutationFn: api.reorder,
onError: () => { setItems(null); toast.error('Σφάλμα αναδιάταξης') },
})
// Simple drag-to-reorder (no extra library — HTML5 DnD)
function handleDragStart(e, id) {
setDragging(id)
e.dataTransfer.effectAllowed = 'move'
}
function handleDragOver(e, id) {
e.preventDefault()
if (dragging == null || dragging === id) return
const from = modifiers.findIndex(m => m.id === dragging)
const to = modifiers.findIndex(m => m.id === id)
if (from === -1 || to === -1) return
const reordered = [...modifiers]
const [moved] = reordered.splice(from, 1)
reordered.splice(to, 0, moved)
setItems(reordered)
}
function handleDrop() {
setDragging(null)
const payload = (items ?? modifiers).map((m, i) => ({ id: m.id, sort_order: i }))
reorder.mutate(payload)
}
const filtered = scopeFilter === 'all' ? modifiers : modifiers.filter(m => m.scope === scopeFilter)
return (
Τροποποιητές Τιμής
Κανόνες που αλλάζουν τιμές αυτόματα ή χειροκίνητα. Η σειρά καθορίζει προτεραιότητα.
{/* Filter pills */}
{[['all','Όλοι'],['global','Καθολικοί'],['item','Item-scope']].map(([v,l]) => (
))}
{isLoading ? (
) : filtered.length === 0 ? (
Δεν υπάρχουν τροποποιητές
Δημιουργήστε έναν για Happy Hour, εποχιακές τιμές κ.λπ.
) : (
e.preventDefault()} onDrop={handleDrop}>
{filtered.map(m => (
handleDragStart(e, m.id)}
onDragOver={e => handleDragOver(e, m.id)}
className={dragging === m.id ? 'opacity-50' : ''}>
{ setEditing(m); setFormTab('info') }} onDelete={setDeleting}
dragHandleProps={{
onMouseDown: () => {},
title: 'Σύρετε για αναδιάταξη',
}} />
))}
)}
{/* Create */}
{showForm && (
{ setShowForm(false); setFormTab('info') }}
maxWidth="max-w-xl" tabs={MODIFIER_TABS} activeTab={formTab} onTabChange={setFormTab}
footer={<>
>}>
create.mutate(body)} onCancel={() => { setShowForm(false); setFormTab('info') }}
saving={create.isPending} groups={groups} activeTab={formTab} onTabChange={setFormTab} />
)}
{/* Edit */}
{editing && (
{ setEditing(null); setFormTab('info') }}
maxWidth="max-w-xl" tabs={MODIFIER_TABS} activeTab={formTab} onTabChange={setFormTab}
footer={<>
>}>
update.mutate({ id: editing.id, body })}
onCancel={() => { setEditing(null); setFormTab('info') }} saving={update.isPending}
groups={groups} activeTab={formTab} onTabChange={setFormTab} />
)}
{/* Delete */}
{deleting && (
remove.mutate(deleting.id)}
onCancel={() => setDeleting(null)} />
)}
)
}