Files
xenia-pos-local/manager_dashboard/src/pages/Management/pricing/DealsTab.jsx
bonamin 34ae328b0d feat: bump client-services (accumulated feature work + deploy fixes)
Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:00:14 +03:00

688 lines
34 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 } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Plus, Gift, Power, Pencil, Trash2, 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: () => client.get('/api/pricing/deals').then(r => r.data),
create: body => client.post('/api/pricing/deals', body).then(r => r.data),
update: (id,b) => client.put(`/api/pricing/deals/${id}`, b).then(r => r.data),
toggle: id => client.patch(`/api/pricing/deals/${id}/toggle`).then(r => r.data),
remove: id => client.delete(`/api/pricing/deals/${id}`),
modifiers: () => client.get('/api/pricing/modifiers').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),
}
const COLORS = [
'#6366f1','#8b5cf6','#ec4899','#f43f5e','#f97316',
'#eab308','#22c55e','#14b8a6','#0ea5e9','#64748b',
]
const CONDITION_TYPES = [
{ value: 'min_item_quantity', label: 'Ελάχιστη ποσότητα αντικειμένου' },
{ value: 'min_order_value', label: 'Ελάχιστη αξία παραγγελίας' },
{ value: 'time_range', label: 'Ώρα ημέρας' },
{ value: 'day_of_week', label: 'Ημέρα εβδομάδας' },
{ value: 'date_range', label: 'Εύρος ημερομηνιών' },
{ value: 'order_channel', label: 'Κανάλι παραγγελίας' },
{ value: 'price_group_active',label: 'Ομάδα τιμής ενεργή' },
]
const DAYS = ['Δευτέρα','Τρίτη','Τετάρτη','Πέμπτη','Παρασκευή','Σάββατο','Κυριακή']
const CHANNELS = ['pos','online','qr','takeaway']
const ACTION_TYPES = [
{ value: 'apply_modifier', label: 'Εφαρμογή τροποποιητή' },
{ value: 'set_price', label: 'Ορισμός τιμής' },
{ value: 'add_amount', label: '± Ποσό €' },
{ value: 'add_percent', label: '± Ποσοστό %' },
{ value: 'free_item', label: 'Δωρεάν προϊόν' },
{ value: 'free_choice', label: 'Επιλογή δωρεάν προϊόντος' },
]
// ─── Reuse condition editor (simplified for deals) ────────────────────────────
function ConditionRow({ cond, onChange, onRemove, groups }) {
const set = (k, v) => onChange({ ...cond, params: { ...cond.params, [k]: v } })
const setType = t => onChange({ condition_type: t, params: {} })
return (
<div className="border border-slate-200 rounded-lg p-3 space-y-2 bg-slate-50">
<div className="flex items-center gap-2">
<select className="input flex-1 text-[12px]" value={cond.condition_type}
onChange={e => setType(e.target.value)}>
{CONDITION_TYPES.map(ct => <option key={ct.value} value={ct.value}>{ct.label}</option>)}
</select>
<button type="button" onClick={onRemove}
className="p-1.5 rounded text-slate-400 hover:text-rose-500 hover:bg-rose-50 transition-colors">
<X className="w-3.5 h-3.5" />
</button>
</div>
{cond.condition_type === 'min_item_quantity' && (
<div>
<label className="block text-[11px] text-slate-500 mb-0.5">Ελάχιστη ποσότητα</label>
<input type="number" min={1} className="input w-28 text-[12px]"
value={cond.params.min ?? 1} onChange={e => set('min', Number(e.target.value))} />
</div>
)}
{cond.condition_type === 'min_order_value' && (
<div>
<label className="block text-[11px] text-slate-500 mb-0.5">Ελάχιστη αξία ()</label>
<input type="number" min={0} step={0.5} className="input w-28 text-[12px]"
value={cond.params.min ?? 0} onChange={e => set('min', Number(e.target.value))} />
</div>
)}
{cond.condition_type === 'time_range' && (
<div className="flex gap-2">
<div className="flex-1">
<label className="block text-[11px] text-slate-500 mb-0.5">Από</label>
<input type="time" className="input w-full text-[12px]" value={cond.params.from ?? ''}
onChange={e => set('from', e.target.value)} />
</div>
<div className="flex-1">
<label className="block text-[11px] text-slate-500 mb-0.5">Έως</label>
<input type="time" className="input w-full text-[12px]" value={cond.params.to ?? ''}
onChange={e => set('to', e.target.value)} />
</div>
</div>
)}
{cond.condition_type === 'day_of_week' && (
<div className="flex flex-wrap gap-1.5">
{DAYS.map((d, i) => (
<button type="button" key={i}
onClick={() => { const days = cond.params.days ?? []; set('days', days.includes(i) ? days.filter(x => x !== i) : [...days, i]) }}
className={`px-2.5 py-1 rounded text-[11px] font-medium transition-colors ${
(cond.params.days ?? []).includes(i) ? 'bg-sky-100 text-sky-700' : 'bg-white border border-slate-200 text-slate-500 hover:bg-slate-50'
}`}>{d.slice(0, 3)}</button>
))}
</div>
)}
{cond.condition_type === 'date_range' && (
<div className="flex gap-2">
<div className="flex-1">
<label className="block text-[11px] text-slate-500 mb-0.5">Από</label>
<input type="date" className="input w-full text-[12px]" value={cond.params.from ?? ''}
onChange={e => set('from', e.target.value)} />
</div>
<div className="flex-1">
<label className="block text-[11px] text-slate-500 mb-0.5">Έως</label>
<input type="date" className="input w-full text-[12px]" value={cond.params.to ?? ''}
onChange={e => set('to', e.target.value)} />
</div>
</div>
)}
{cond.condition_type === 'order_channel' && (
<div className="flex flex-wrap gap-1.5">
{CHANNELS.map(ch => (
<button type="button" key={ch}
onClick={() => { const channels = cond.params.channels ?? []; set('channels', channels.includes(ch) ? channels.filter(x => x !== ch) : [...channels, ch]) }}
className={`px-2.5 py-1 rounded text-[11px] font-medium transition-colors ${
(cond.params.channels ?? []).includes(ch) ? 'bg-sky-100 text-sky-700' : 'bg-white border border-slate-200 text-slate-500 hover:bg-slate-50'
}`}>{ch}</button>
))}
</div>
)}
{cond.condition_type === 'price_group_active' && (
<div>
<select className="input w-full text-[12px]" value={cond.params.price_group_id ?? ''}
onChange={e => set('price_group_id', Number(e.target.value))}>
<option value=""> Επιλογή ομάδας </option>
{(groups ?? []).map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
</select>
</div>
)}
</div>
)
}
// ─── Shared multi-picker (same logic as ModifiersTab) ─────────────────────────
function MultiPicker({ items, selected, onToggle, labelKey = 'name', valueKey = 'id' }) {
return (
<div className="border border-slate-200 rounded-lg overflow-hidden">
{items.length === 0 && (
<p className="text-[11px] text-slate-400 px-3 py-2">Δεν βρέθηκαν αποτελέσματα</p>
)}
{items.map(item => {
const val = item[valueKey]
const checked = selected.includes(val)
return (
<div key={val} onClick={() => 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'
}`}>
<div className={`w-4 h-4 rounded border flex items-center justify-center shrink-0 transition-colors ${
checked ? 'bg-sky-500 border-sky-500' : 'border-slate-300 bg-white'
}`}>
{checked && <svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4l3 3 5-6" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}
</div>
{item[labelKey]}
</div>
)
})}
</div>
)
}
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 (
<div className="space-y-2">
{allTags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{allTags.map(tag => {
const active = selected.includes(tag)
return (
<button type="button" key={tag} onClick={() => onToggle(tag)}
className={`px-2 py-0.5 rounded-full text-[11px] font-medium border transition-colors ${
active ? 'bg-sky-500 border-sky-500 text-white' : 'border-slate-300 text-slate-600 hover:border-sky-400 hover:text-sky-600'
}`}>{tag}</button>
)
})}
</div>
)}
{selected.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{selected.map(tag => (
<span key={tag} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-sky-100 text-sky-700 text-[11px] font-medium">
{tag}
<button type="button" onClick={() => onToggle(tag)} className="hover:text-sky-900">
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
<div className="flex gap-2">
<input className="input flex-1 text-[12px]" placeholder="Νέο tag..." value={draft}
onChange={e => setDraft(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAdd() } }} />
<button type="button" onClick={handleAdd}
className="px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 text-[12px] hover:bg-slate-200 transition-colors">
+
</button>
</div>
</div>
)
}
// ─── Deal target row ──────────────────────────────────────────────────────────
function DealTargetRow({ target, onChange, onRemove, products, categories, 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 (
<div className="border border-slate-200 rounded-lg p-3 space-y-2 bg-slate-50">
<div className="flex items-center gap-2">
<select className="input flex-1 text-[12px]" value={target.target_type}
onChange={e => onChange({ target_type: e.target.value, target_id: null, target_tag: null, target_ids: [], target_tags: [] })}>
<option value="any">Οποιοδήποτε προϊόν</option>
<option value="item">Προϊόντα</option>
<option value="category">Κατηγορίες</option>
<option value="tag">Ετικέτες (tags)</option>
</select>
<button type="button" onClick={onRemove}
className="p-1.5 rounded text-slate-400 hover:text-rose-500 hover:bg-rose-50 transition-colors">
<X className="w-3.5 h-3.5" />
</button>
</div>
{target.target_type === 'item' && (
<MultiPicker items={products} selected={ids} onToggle={toggleId} />
)}
{target.target_type === 'category' && (
<MultiPicker items={categories} selected={ids} onToggle={toggleId} />
)}
{target.target_type === 'tag' && (
<TagPicker selected={tags} onToggle={toggleTag} onAdd={tag => setTags([...tags, tag])} allTags={allTags} />
)}
</div>
)
}
// ─── Deal form ────────────────────────────────────────────────────────────────
const DEAL_TABS = [
{ key: 'info', label: 'Πληροφορίες' },
{ key: 'trigger', label: 'Trigger & Συνθήκες' },
{ key: 'action', label: 'Ενέργεια' },
]
function DealForm({ initial, onSave, onCancel, saving, modifiers, 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: 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[6], is_active: true, sort_order: 0,
action_type: 'free_item',
action_modifier_id: null, action_value: null,
action_free_item_id: null,
action_free_target_type: 'item', action_free_target_ids: [],
action_free_quantity: 1,
conditions: [], targets: [{ target_type: 'any', target_id: null, target_tag: null, target_ids: [], target_tags: [] }],
}
const [f, setF] = useState(initial ? {
...blank, ...initial,
conditions: initial.conditions ?? [],
targets: (initial.targets ?? [{ target_type: 'any', target_id: null, target_tag: null, target_ids: [], target_tags: [] }]).map(normalizeTarget),
action_free_target_ids: initial.action_free_target_ids ?? [],
} : blank)
const set = (k, v) => setF(p => ({ ...p, [k]: v }))
const addCond = () => set('conditions', [...f.conditions, { condition_type: 'min_item_quantity', params: { min: 1 } }])
const updCond = (i, c) => set('conditions', f.conditions.map((x, j) => j === i ? c : x))
const remCond = i => set('conditions', f.conditions.filter((_, j) => j !== i))
const addTarget = () => set('targets', [...f.targets, { target_type: 'any', target_id: null, target_tag: null, target_ids: [], target_tags: [] }])
const updTarget = (i, t) => set('targets', f.targets.map((x, j) => j === i ? t : x))
const remTarget = i => set('targets', f.targets.filter((_, j) => j !== i))
const toggleFreeId = id => {
const ids = f.action_free_target_ids ?? []
set('action_free_target_ids', ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id])
}
function submit(e) {
e.preventDefault()
if (!f.name.trim()) return toast.error('Απαιτείται όνομα')
onSave({
...f,
action_free_target_ids: f.action_free_target_ids?.length ? f.action_free_target_ids : null,
})
}
return (
<form id="deal-form" onSubmit={submit} className="space-y-5">
{/* ── Info tab ── */}
{activeTab === 'info' && (
<>
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2">
<label className="block text-[12px] font-medium text-slate-600 mb-1">Όνομα προσφοράς</label>
<input className="input w-full" value={f.name} onChange={e => set('name', e.target.value)}
placeholder="π.χ. 3+1 Μπύρες" autoFocus />
</div>
<div className="col-span-2">
<label className="block text-[12px] font-medium text-slate-600 mb-1">Περιγραφή</label>
<input className="input w-full" value={f.description ?? ''}
onChange={e => set('description', e.target.value)} />
</div>
</div>
<div className="flex items-center justify-between">
<div>
<label className="block text-[12px] font-medium text-slate-600 mb-1.5">Χρώμα</label>
<div className="flex flex-wrap gap-2">
{COLORS.map(c => (
<button type="button" key={c} onClick={() => set('color', c)}
className={`w-6 h-6 rounded-full transition-transform ${f.color === c ? 'ring-2 ring-offset-1 ring-slate-400 scale-110' : 'hover:scale-105'}`}
style={{ background: c }} />
))}
</div>
</div>
<label className="flex items-center gap-2 cursor-pointer select-none">
<div onClick={() => set('is_active', !f.is_active)}
className={`relative rounded-full transition-colors ${f.is_active ? 'bg-sky-500' : 'bg-slate-200'}`}
style={{ width: 32, height: 18 }}>
<span className={`absolute top-0.5 left-0.5 w-3.5 h-3.5 rounded-full bg-white shadow transition-transform ${f.is_active ? 'translate-x-3.5' : ''}`} />
</div>
<span className="text-[12px] text-slate-700">Ενεργή</span>
</label>
</div>
</>
)}
{/* ── Trigger & Conditions tab ── */}
{activeTab === 'trigger' && (
<>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<span className="text-[12px] font-semibold text-slate-600 uppercase tracking-wide">Trigger</span>
<span className="text-[11px] text-slate-400 ml-1.5">Αγορά αυτών</span>
</div>
<button type="button" onClick={addTarget}
className="text-[12px] text-sky-600 hover:text-sky-700 flex items-center gap-1">
<Plus className="w-3.5 h-3.5" />Προσθήκη
</button>
</div>
<p className="text-[11px] text-slate-400">
Ποια προϊόντα πρέπει να υπάρχουν στην παραγγελία για να ενεργοποιηθεί η προσφορά;
</p>
{f.targets.map((t, i) => (
<DealTargetRow key={i} target={t} onChange={v => updTarget(i, v)} onRemove={() => remTarget(i)}
products={products} categories={categories} allTags={allTags} />
))}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<span className="text-[12px] font-semibold text-slate-600 uppercase tracking-wide">Επιπλέον Συνθήκες</span>
<span className="text-[11px] text-slate-400 ml-1.5">(προαιρετικά)</span>
</div>
<button type="button" onClick={addCond}
className="text-[12px] text-sky-600 hover:text-sky-700 flex items-center gap-1">
<Plus className="w-3.5 h-3.5" />Προσθήκη
</button>
</div>
{f.conditions.length === 0 && (
<p className="text-[12px] text-slate-400 bg-slate-50 rounded-lg px-3 py-2.5">
Χωρίς επιπλέον συνθήκες αρκεί μόνο το trigger.
</p>
)}
{f.conditions.map((c, i) => (
<ConditionRow key={i} cond={c} onChange={v => updCond(i, v)} onRemove={() => remCond(i)} groups={groups} />
))}
</div>
</>
)}
{/* ── Action tab ── */}
{activeTab === 'action' && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
{ACTION_TYPES.map(({ value, label }) => (
<button type="button" key={value} onClick={() => set('action_type', value)}
className={`py-2 rounded-lg border text-[12px] font-medium transition-colors text-center ${
f.action_type === value ? 'border-sky-500 bg-sky-50 text-sky-700' : 'border-slate-200 text-slate-500 hover:bg-slate-50'
}`}>{label}</button>
))}
</div>
{f.action_type === 'apply_modifier' && (
<div>
<label className="block text-[11px] text-slate-500 mb-1">Τροποποιητής (χωρίς συνθήκες)</label>
<select className="input w-full text-[12px]" value={f.action_modifier_id ?? ''}
onChange={e => set('action_modifier_id', Number(e.target.value))}>
<option value=""> Επιλογή </option>
{(modifiers ?? []).filter(m => !m.conditions?.length).map(m => (
<option key={m.id} value={m.id}>{m.name}</option>
))}
</select>
<p className="text-[11px] text-slate-400 mt-1">Εμφανίζονται μόνο τροποποιητές χωρίς συνθήκες (χειροκίνητοι).</p>
</div>
)}
{(f.action_type === 'set_price' || f.action_type === 'add_amount' || f.action_type === 'add_percent') && (
<div>
<label className="block text-[11px] text-slate-500 mb-1">
{f.action_type === 'set_price' ? 'Νέα τιμή (€)' :
f.action_type === 'add_amount' ? 'Ποσό (€, αρνητικό = έκπτωση)' :
'Ποσοστό (%, αρνητικό = έκπτωση)'}
</label>
<input type="number" step={0.1} className="input w-36 text-[12px]"
value={f.action_value ?? 0} onChange={e => set('action_value', Number(e.target.value))} />
</div>
)}
{f.action_type === 'free_item' && (
<div className="space-y-3">
<div>
<label className="block text-[11px] text-slate-500 mb-1">Δωρεάν προϊόν</label>
<select className="input w-full text-[12px]" value={f.action_free_item_id ?? ''}
onChange={e => set('action_free_item_id', Number(e.target.value))}>
<option value=""> Επιλογή </option>
{products.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div>
<label className="block text-[11px] text-slate-500 mb-1">Ποσότητα</label>
<input type="number" min={1} className="input w-20 text-[12px]"
value={f.action_free_quantity} onChange={e => set('action_free_quantity', Number(e.target.value))} />
</div>
</div>
)}
{f.action_type === 'free_choice' && (
<div className="space-y-2">
<div className="flex gap-2">
{[['item','Προϊόντα'],['category','Κατηγορία'],['tag','Ετικέτα']].map(([v,l]) => (
<button type="button" key={v} onClick={() => { set('action_free_target_type', v); set('action_free_target_ids', []) }}
className={`px-3 py-1 rounded text-[12px] font-medium border transition-colors ${
f.action_free_target_type === v ? 'border-sky-500 bg-sky-50 text-sky-700' : 'border-slate-200 text-slate-500 hover:bg-slate-50'
}`}>{l}</button>
))}
</div>
{f.action_free_target_type === 'item' && (
<div className="max-h-40 overflow-y-auto border border-slate-200 rounded-lg p-2 space-y-1">
{products.map(p => (
<label key={p.id} className="flex items-center gap-2 cursor-pointer py-0.5">
<input type="checkbox" className="rounded"
checked={(f.action_free_target_ids ?? []).includes(p.id)}
onChange={() => toggleFreeId(p.id)} />
<span className="text-[12px] text-slate-700">{p.name}</span>
</label>
))}
</div>
)}
{f.action_free_target_type === 'category' && (
<div className="max-h-40 overflow-y-auto border border-slate-200 rounded-lg p-2 space-y-1">
{categories.map(c => (
<label key={c.id} className="flex items-center gap-2 cursor-pointer py-0.5">
<input type="checkbox" className="rounded"
checked={(f.action_free_target_ids ?? []).includes(c.id)}
onChange={() => toggleFreeId(c.id)} />
<span className="text-[12px] text-slate-700">{c.name}</span>
</label>
))}
</div>
)}
{f.action_free_target_type === 'tag' && (
<div>
<label className="block text-[11px] text-slate-500 mb-1">Ετικέτες (μία ανά γραμμή)</label>
<textarea className="input w-full text-[12px] font-mono" rows={3}
value={(f.action_free_target_ids ?? []).join('\n')}
onChange={e => set('action_free_target_ids', e.target.value.split('\n').map(s => s.trim()).filter(Boolean))} />
</div>
)}
<div>
<label className="block text-[11px] text-slate-500 mb-1">Ποσότητα δωρεάν</label>
<input type="number" min={1} className="input w-20 text-[12px]"
value={f.action_free_quantity} onChange={e => set('action_free_quantity', Number(e.target.value))} />
</div>
</div>
)}
</div>
)}
</form>
)
}
// ─── Deal card ────────────────────────────────────────────────────────────────
function DealCard({ deal, onEdit, onDelete }) {
const qc = useQueryClient()
const toggle = useMutation({
mutationFn: () => api.toggle(deal.id),
onSuccess: () => qc.invalidateQueries({ queryKey: ['pricing-deals'] }),
onError: () => toast.error('Σφάλμα'),
})
const actionLabel = ACTION_TYPES.find(a => a.value === deal.action_type)?.label ?? deal.action_type
return (
<div className={`bg-white rounded-xl border shadow-sm p-4 flex items-start gap-3 transition-opacity ${deal.is_active ? 'border-slate-200' : 'border-slate-100 opacity-60'}`}>
<div className="w-3 h-3 rounded-full mt-1 shrink-0" style={{ background: deal.color ?? '#22c55e' }} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[14px] font-semibold text-slate-800">{deal.name}</span>
<span className={`text-[11px] font-medium px-1.5 py-0.5 rounded-full ${
deal.is_active ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}>{deal.is_active ? 'Ενεργή' : 'Ανενεργή'}</span>
<span className="text-[11px] text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded-full">{actionLabel}</span>
</div>
{deal.description && <p className="text-[12px] text-slate-500 mt-0.5">{deal.description}</p>}
<p className="text-[11px] text-slate-400 mt-1">
{deal.conditions?.length ?? 0} συνθήκες · {deal.targets?.length ?? 0} targets
</p>
</div>
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => toggle.mutate()}
className={`p-1.5 rounded-lg transition-colors ${
deal.is_active ? 'text-emerald-600 hover:bg-emerald-50' : 'text-slate-400 hover:bg-slate-100'
}`}>
<Power className="w-4 h-4" />
</button>
<button onClick={() => onEdit(deal)}
className="p-1.5 rounded-lg text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-colors">
<Pencil className="w-4 h-4" />
</button>
<button onClick={() => onDelete(deal)}
className="p-1.5 rounded-lg text-slate-400 hover:bg-rose-50 hover:text-rose-500 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
)
}
// ─── Main tab ─────────────────────────────────────────────────────────────────
export default function DealsTab() {
const qc = useQueryClient()
const [showForm, setShowForm] = useState(false)
const [editing, setEditing] = useState(null)
const [deleting, setDeleting] = useState(null)
const [formTab, setFormTab] = useState('info')
const { data: deals = [], isLoading } = useQuery({ queryKey: ['pricing-deals'], queryFn: api.list })
const { data: modifiers = [] } = useQuery({ queryKey: ['pricing-modifiers'], queryFn: api.modifiers })
const { data: groups = [] } = useQuery({ queryKey: ['pricing-groups'], queryFn: () => client.get('/api/pricing/groups').then(r => r.data) })
const create = useMutation({
mutationFn: api.create,
onSuccess: () => { qc.invalidateQueries({ queryKey: ['pricing-deals'] }); setShowForm(false); toast.success('Δημιουργήθηκε') },
onError: () => toast.error('Σφάλμα'),
})
const update = useMutation({
mutationFn: ({ id, body }) => api.update(id, body),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['pricing-deals'] }); setEditing(null); toast.success('Αποθηκεύτηκε') },
onError: () => toast.error('Σφάλμα'),
})
const remove = useMutation({
mutationFn: api.remove,
onSuccess: () => { qc.invalidateQueries({ queryKey: ['pricing-deals'] }); setDeleting(null); toast.success('Διαγράφηκε') },
onError: () => toast.error('Σφάλμα'),
})
const formProps = { modifiers, groups }
return (
<div className="p-6 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-[14px] font-semibold text-slate-800">Προσφορές</h2>
<p className="text-[12px] text-slate-500 mt-0.5">
Αυτόματες δράσεις που ενεργοποιούνται όταν ο σερβιτόρος προσθέτει συγκεκριμένα προϊόντα στην παραγγελία.
</p>
</div>
<Button variant="primary" onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-1.5" />Νέα Προσφορά
</Button>
</div>
<div className="flex items-start gap-3 bg-sky-50 border border-sky-200 rounded-lg px-4 py-3">
<Info className="w-4 h-4 text-sky-500 shrink-0 mt-0.5" />
<p className="text-[12px] text-sky-700">
Όταν μια προσφορά ενεργοποιηθεί, ο σερβιτόρος λαμβάνει prompt στην εφαρμογή και επιλέγει αν θα την εφαρμόσει.
Τίποτα δεν εφαρμόζεται αυτόματα χωρίς επιβεβαίωση.
</p>
</div>
{isLoading ? (
<div className="flex justify-center py-12">
<div className="w-5 h-5 rounded-full border-2 border-sky-500 border-t-transparent animate-spin" />
</div>
) : deals.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-400">
<Gift className="w-10 h-10 mb-3 opacity-30" />
<p className="text-[13px]">Δεν υπάρχουν προσφορές</p>
<p className="text-[12px] mt-1">π.χ. «Αγόρασε 3 μπύρες, πάρε 1 δωρεάν»</p>
</div>
) : (
<div className="space-y-3">
{deals.map(d => (
<DealCard key={d.id} deal={d} onEdit={d => { setEditing(d); setFormTab('info') }} onDelete={setDeleting} />
))}
</div>
)}
{showForm && (
<Modal title="Νέα Προσφορά" onClose={() => { setShowForm(false); setFormTab('info') }}
maxWidth="max-w-xl" tabs={DEAL_TABS} activeTab={formTab} onTabChange={setFormTab}
footer={<>
<Button type="button" variant="secondary" onClick={() => { setShowForm(false); setFormTab('info') }}>Ακύρωση</Button>
<Button type="submit" form="deal-form" variant="primary" disabled={create.isPending}>
{create.isPending ? 'Αποθήκευση...' : 'Αποθήκευση'}
</Button>
</>}>
<DealForm onSave={body => create.mutate(body)} onCancel={() => { setShowForm(false); setFormTab('info') }}
saving={create.isPending} {...formProps} activeTab={formTab} onTabChange={setFormTab} />
</Modal>
)}
{editing && (
<Modal title="Επεξεργασία Προσφοράς" onClose={() => { setEditing(null); setFormTab('info') }}
maxWidth="max-w-xl" tabs={DEAL_TABS} activeTab={formTab} onTabChange={setFormTab}
footer={<>
<Button type="button" variant="secondary" onClick={() => { setEditing(null); setFormTab('info') }}>Ακύρωση</Button>
<Button type="submit" form="deal-form" variant="primary" disabled={update.isPending}>
{update.isPending ? 'Αποθήκευση...' : 'Αποθήκευση'}
</Button>
</>}>
<DealForm initial={editing} onSave={body => update.mutate({ id: editing.id, body })}
onCancel={() => { setEditing(null); setFormTab('info') }} saving={update.isPending}
{...formProps} activeTab={formTab} onTabChange={setFormTab} />
</Modal>
)}
{deleting && (
<ConfirmModal title="Διαγραφή Προσφοράς"
message={`Θέλετε να διαγράψετε την προσφορά "${deleting.name}";`}
confirmLabel="Διαγραφή"
onConfirm={() => remove.mutate(deleting.id)}
onCancel={() => setDeleting(null)} />
)}
</div>
)
}