@fayz-ai/plugin-menu 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,694 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
2
- import {
3
- Plus,
4
- Search,
5
- GripVertical,
6
- ChevronDown,
7
- ChevronRight,
8
- MoreHorizontal,
9
- Star,
10
- Loader2,
11
- UtensilsCrossed,
12
- } from 'lucide-react'
13
- import {
14
- Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter,
15
- Button, Input,
16
- Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
17
- } from '@fayz-ai/ui'
18
- import { useMenuConfig, useMenuStore, formatCurrency } from '../MenuContext'
19
- import { useTranslation } from '@fayz-ai/core'
20
- import { useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
21
- import type { MenuCategory, MenuItem } from '../types'
22
-
23
- // ---------------------------------------------------------------------------
24
- // Create forms (category / item) — design-system Modal
25
- // ---------------------------------------------------------------------------
26
-
27
- const UNCATEGORIZED = '__none'
28
- const fieldLabel = 'text-sm font-medium text-foreground'
29
-
30
- function CategoryModal({ open, onClose }: { open: boolean; onClose: () => void }) {
31
- const t = useTranslation()
32
- const createCategory = useMenuStore((s) => s.createCategory)
33
- const [name, setName] = useState('')
34
- const [saving, setSaving] = useState(false)
35
-
36
- const submit = async () => {
37
- if (!name.trim()) return
38
- setSaving(true)
39
- try {
40
- await createCategory({ name: name.trim() })
41
- setName('')
42
- onClose()
43
- } catch {
44
- // store surfaces a toast on failure
45
- } finally {
46
- setSaving(false)
47
- }
48
- }
49
-
50
- return (
51
- <Modal open={open} onOpenChange={(o) => { if (!o) onClose() }}>
52
- <ModalContent size="sm">
53
- <ModalHeader>
54
- <ModalTitle>{t('menu.manager.addCategory')}</ModalTitle>
55
- </ModalHeader>
56
- <ModalBody className="space-y-4">
57
- <label className="block space-y-1.5">
58
- <span className={fieldLabel}>{t('menu.form.categoryName')}</span>
59
- <Input
60
- autoFocus
61
- value={name}
62
- onChange={(e) => setName(e.target.value)}
63
- onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
64
- />
65
- </label>
66
- </ModalBody>
67
- <ModalFooter>
68
- <Button variant="outline" onClick={onClose}>{t('menu.form.cancel')}</Button>
69
- <Button onClick={submit} disabled={saving || !name.trim()}>
70
- {saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
71
- {t('menu.form.save')}
72
- </Button>
73
- </ModalFooter>
74
- </ModalContent>
75
- </Modal>
76
- )
77
- }
78
-
79
- function ItemModal({
80
- open,
81
- categories,
82
- defaultCategoryId,
83
- onClose,
84
- }: {
85
- open: boolean
86
- categories: MenuCategory[]
87
- defaultCategoryId?: string
88
- onClose: () => void
89
- }) {
90
- const t = useTranslation()
91
- const config = useMenuConfig()
92
- const createMenuItem = useMenuStore((s) => s.createMenuItem)
93
- const guardMenuItems = useLimitGuard('menu_items')
94
- const [name, setName] = useState('')
95
- const [price, setPrice] = useState('')
96
- const [categoryId, setCategoryId] = useState(defaultCategoryId ?? UNCATEGORIZED)
97
- const [description, setDescription] = useState('')
98
- const [saving, setSaving] = useState(false)
99
-
100
- const submit = async () => {
101
- if (!name.trim()) return
102
- // Client-side plan guard before the store create. (Note: this dialog also
103
- // has the known board bug B29 — out of scope here, guard only.)
104
- if ((await guardMenuItems()) === 'blocked') return
105
- setSaving(true)
106
- try {
107
- await createMenuItem({
108
- name: name.trim(),
109
- price: Number(price) || 0,
110
- categoryId: categoryId === UNCATEGORIZED ? undefined : categoryId,
111
- description: description.trim() || undefined,
112
- })
113
- invalidateLimit('menu_items')
114
- onClose()
115
- } catch {
116
- // store surfaces a toast on failure
117
- } finally {
118
- setSaving(false)
119
- }
120
- }
121
-
122
- return (
123
- <Modal open={open} onOpenChange={(o) => { if (!o) onClose() }}>
124
- <ModalContent size="md">
125
- <ModalHeader>
126
- <ModalTitle>{t('menu.manager.addProduct')}</ModalTitle>
127
- </ModalHeader>
128
- <ModalBody className="space-y-4">
129
- <label className="block space-y-1.5">
130
- <span className={fieldLabel}>{t('menu.form.itemName')}</span>
131
- <Input autoFocus value={name} onChange={(e) => setName(e.target.value)} />
132
- </label>
133
- <div className="grid grid-cols-2 gap-3">
134
- <label className="block space-y-1.5">
135
- <span className={fieldLabel}>{t('menu.manager.price')} ({config.currency.symbol})</span>
136
- <Input
137
- type="number"
138
- min={0}
139
- step="0.01"
140
- value={price}
141
- onChange={(e) => setPrice(e.target.value)}
142
- placeholder="0"
143
- />
144
- </label>
145
- <label className="block space-y-1.5">
146
- <span className={fieldLabel}>{t('menu.form.category')}</span>
147
- <Select value={categoryId} onValueChange={setCategoryId}>
148
- <SelectTrigger><SelectValue /></SelectTrigger>
149
- <SelectContent>
150
- <SelectItem value={UNCATEGORIZED}>{t('menu.form.uncategorized')}</SelectItem>
151
- {categories.map((cat) => (
152
- <SelectItem key={cat.id} value={cat.id}>{cat.name}</SelectItem>
153
- ))}
154
- </SelectContent>
155
- </Select>
156
- </label>
157
- </div>
158
- <label className="block space-y-1.5">
159
- <span className={fieldLabel}>
160
- {t('menu.form.description')}{' '}
161
- <span className="text-muted-foreground font-normal">({t('menu.form.optional')})</span>
162
- </span>
163
- <textarea
164
- value={description}
165
- onChange={(e) => setDescription(e.target.value)}
166
- rows={2}
167
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
168
- />
169
- </label>
170
- </ModalBody>
171
- <ModalFooter>
172
- <Button variant="outline" onClick={onClose}>{t('menu.form.cancel')}</Button>
173
- <Button onClick={submit} disabled={saving || !name.trim()}>
174
- {saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
175
- {t('menu.form.save')}
176
- </Button>
177
- </ModalFooter>
178
- </ModalContent>
179
- </Modal>
180
- )
181
- }
182
-
183
- // ---------------------------------------------------------------------------
184
- // Toggle — inline switch component
185
- // ---------------------------------------------------------------------------
186
-
187
- function Toggle({
188
- checked,
189
- onChange,
190
- size = 'sm',
191
- }: {
192
- checked: boolean
193
- onChange: (v: boolean) => void
194
- size?: 'sm' | 'xs'
195
- }) {
196
- const h = size === 'xs' ? 'h-4 w-7' : 'h-5 w-9'
197
- const dot = size === 'xs' ? 'h-3 w-3' : 'h-4 w-4'
198
- const translate = size === 'xs' ? 'translate-x-[13px]' : 'translate-x-[18px]'
199
- return (
200
- <button
201
- type="button"
202
- onClick={() => onChange(!checked)}
203
- className={`relative inline-flex ${h} items-center rounded-full transition-colors ${
204
- checked ? 'bg-primary' : 'bg-muted-foreground/30'
205
- }`}
206
- >
207
- <span
208
- className={`pointer-events-none block ${dot} rounded-full bg-white shadow-sm transition-transform ${
209
- checked ? translate : 'translate-x-[2px]'
210
- }`}
211
- />
212
- </button>
213
- )
214
- }
215
-
216
- // ---------------------------------------------------------------------------
217
- // Skeleton loader
218
- // ---------------------------------------------------------------------------
219
-
220
- function CategorySkeleton() {
221
- return (
222
- <div className="rounded-lg border bg-card animate-pulse">
223
- <div className="flex items-center gap-3 px-4 py-3">
224
- <div className="h-4 w-4 rounded bg-muted" />
225
- <div className="h-4 w-4 rounded bg-muted" />
226
- <div className="h-4 w-32 rounded bg-muted flex-1" />
227
- <div className="h-4 w-16 rounded bg-muted" />
228
- </div>
229
- </div>
230
- )
231
- }
232
-
233
- // ---------------------------------------------------------------------------
234
- // Category section
235
- // ---------------------------------------------------------------------------
236
-
237
- function CategorySection({
238
- category,
239
- items,
240
- expanded,
241
- onToggle,
242
- onAddProduct,
243
- }: {
244
- category: MenuCategory
245
- items: MenuItem[]
246
- expanded: boolean
247
- onToggle: () => void
248
- onAddProduct: () => void
249
- }) {
250
- const t = useTranslation()
251
- const config = useMenuConfig()
252
- const toggleAvailability = useMenuStore((s) => s.toggleAvailability)
253
-
254
- const handleToggle = async (
255
- id: string,
256
- field: 'availableForPos' | 'availableForDelivery',
257
- current: boolean,
258
- ) => {
259
- await toggleAvailability(id, field, !current)
260
- }
261
-
262
- const handleFeatureToggle = async (id: string, current: boolean) => {
263
- await toggleAvailability(id, 'isFeatured' as any, !current)
264
- }
265
-
266
- return (
267
- <div className="rounded-lg border bg-card">
268
- {/* Category header */}
269
- <div className="flex items-center gap-3 px-4 py-3">
270
- <GripVertical className="h-4 w-4 text-muted-foreground/40 cursor-grab" />
271
- <button type="button" onClick={onToggle} className="p-0.5">
272
- {expanded ? (
273
- <ChevronDown className="h-4 w-4" />
274
- ) : (
275
- <ChevronRight className="h-4 w-4" />
276
- )}
277
- </button>
278
- <h3 className="text-sm font-semibold flex-1">{category.name}</h3>
279
- <div className="flex items-center gap-4 text-xs text-muted-foreground">
280
- <span>{t('menu.manager.itemsCount', { count: items.length })}</span>
281
- </div>
282
- <button className="p-1 text-muted-foreground hover:text-foreground rounded">
283
- <MoreHorizontal className="h-4 w-4" />
284
- </button>
285
- </div>
286
-
287
- {expanded && (
288
- <>
289
- {/* Add product button */}
290
- <div className="px-4 pb-2">
291
- <button
292
- type="button"
293
- onClick={onAddProduct}
294
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground border border-dashed rounded-md px-3 py-1.5 hover:bg-muted/30 transition-colors"
295
- >
296
- <Plus className="h-3 w-3" />
297
- {t('menu.manager.addProduct')}
298
- </button>
299
- </div>
300
-
301
- {/* Items table */}
302
- {/* drift-allow: raw-table — interactive cells (drag handles, availability toggles, inline price/feature controls) don't fit DataTable */}
303
- {items.length > 0 ? (
304
- <div className="overflow-x-auto">
305
- <table className="w-full">
306
- <thead>
307
- <tr className="border-t bg-muted/30 text-[10px] uppercase tracking-wider text-muted-foreground">
308
- <th className="text-left py-2 pl-4 pr-2 font-medium">
309
- {t('menu.manager.item')}
310
- </th>
311
- <th className="py-2 px-2 font-medium text-center w-16">
312
- {t('menu.manager.pos')}
313
- </th>
314
- <th className="py-2 px-2 font-medium text-center w-16">
315
- {t('menu.manager.delivery')}
316
- </th>
317
- <th className="py-2 px-2 font-medium text-center w-28">
318
- {t('menu.manager.price')}
319
- </th>
320
- {config.modules.deliveryPricing && (
321
- <th className="py-2 px-2 font-medium text-center w-28">
322
- {t('menu.manager.deliveryPrice')}
323
- </th>
324
- )}
325
- <th className="py-2 px-2 font-medium text-center w-16">
326
- {t('menu.manager.featured')}
327
- </th>
328
- <th className="py-2 pr-4 w-8" />
329
- </tr>
330
- </thead>
331
- <tbody className="divide-y">
332
- {items.map((item) => (
333
- <tr
334
- key={item.id}
335
- className="hover:bg-muted/20 transition-colors"
336
- >
337
- <td className="py-2.5 pl-4 pr-2">
338
- <div className="flex items-center gap-3">
339
- <GripVertical className="h-3.5 w-3.5 text-muted-foreground/30 cursor-grab shrink-0" />
340
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted/50 overflow-hidden">
341
- {item.imageUrl ? (
342
- <img
343
- src={item.imageUrl}
344
- alt=""
345
- className="h-full w-full object-cover"
346
- />
347
- ) : (
348
- <UtensilsCrossed className="h-4 w-4 text-muted-foreground/40" />
349
- )}
350
- </div>
351
- <div className="min-w-0">
352
- <p className="text-sm font-medium truncate">
353
- {item.name}
354
- </p>
355
- {item.description && (
356
- <p className="text-xs text-muted-foreground truncate max-w-[280px]">
357
- {item.description}
358
- </p>
359
- )}
360
- </div>
361
- </div>
362
- </td>
363
- <td className="py-2.5 px-2 text-center">
364
- <Toggle
365
- checked={item.availableForPos}
366
- onChange={() =>
367
- handleToggle(
368
- item.id,
369
- 'availableForPos',
370
- item.availableForPos,
371
- )
372
- }
373
- size="xs"
374
- />
375
- </td>
376
- <td className="py-2.5 px-2 text-center">
377
- <Toggle
378
- checked={item.availableForDelivery}
379
- onChange={() =>
380
- handleToggle(
381
- item.id,
382
- 'availableForDelivery',
383
- item.availableForDelivery,
384
- )
385
- }
386
- size="xs"
387
- />
388
- </td>
389
- <td className="py-2.5 px-2 text-center">
390
- <span className="text-sm font-medium">
391
- {formatCurrency(item.price, config.currency)}
392
- </span>
393
- </td>
394
- {config.modules.deliveryPricing && (
395
- <td className="py-2.5 px-2 text-center">
396
- <span className="text-sm text-muted-foreground">
397
- {item.deliveryPrice != null
398
- ? formatCurrency(
399
- item.deliveryPrice,
400
- config.currency,
401
- )
402
- : '\u2014'}
403
- </span>
404
- </td>
405
- )}
406
- <td className="py-2.5 px-2 text-center">
407
- <button
408
- onClick={() =>
409
- handleFeatureToggle(item.id, item.isFeatured)
410
- }
411
- className={`p-1 rounded ${
412
- item.isFeatured
413
- ? 'text-amber-500'
414
- : 'text-muted-foreground/30 hover:text-muted-foreground'
415
- }`}
416
- >
417
- <Star
418
- className={`h-4 w-4 ${
419
- item.isFeatured ? 'fill-current' : ''
420
- }`}
421
- />
422
- </button>
423
- </td>
424
- <td className="py-2.5 pr-4">
425
- <button className="p-1 text-muted-foreground hover:text-foreground rounded">
426
- <MoreHorizontal className="h-3.5 w-3.5" />
427
- </button>
428
- </td>
429
- </tr>
430
- ))}
431
- </tbody>
432
- </table>
433
- </div>
434
- ) : (
435
- <div className="border-t px-4 py-6 text-center text-xs text-muted-foreground">
436
- {t('menu.manager.noItems')}
437
- </div>
438
- )}
439
- </>
440
- )}
441
- </div>
442
- )
443
- }
444
-
445
- // ---------------------------------------------------------------------------
446
- // Main view
447
- // ---------------------------------------------------------------------------
448
-
449
- export function MenuManagerView() {
450
- const t = useTranslation()
451
- const config = useMenuConfig()
452
-
453
- const categories = useMenuStore((s) => s.categories)
454
- const categoriesLoading = useMenuStore((s) => s.categoriesLoading)
455
- const menuItems = useMenuStore((s) => s.menuItems)
456
- const menuItemsLoading = useMenuStore((s) => s.menuItemsLoading)
457
- const fetchCategories = useMenuStore((s) => s.fetchCategories)
458
- const fetchMenuItems = useMenuStore((s) => s.fetchMenuItems)
459
-
460
- const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
461
- new Set(),
462
- )
463
- const [search, setSearch] = useState('')
464
- const [categoryFilter, setCategoryFilter] = useState('all')
465
- const [showCategoryModal, setShowCategoryModal] = useState(false)
466
- const [showItemModal, setShowItemModal] = useState(false)
467
- const [itemModalCategory, setItemModalCategory] = useState<string | undefined>(undefined)
468
-
469
- const openItemModal = (categoryId?: string) => {
470
- setItemModalCategory(categoryId)
471
- setShowItemModal(true)
472
- }
473
-
474
- // ---- Load data on mount ----
475
- useEffect(() => {
476
- fetchCategories()
477
- fetchMenuItems({})
478
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
479
-
480
- // ---- Expand all once categories load ----
481
- useEffect(() => {
482
- if (categories.length > 0 && expandedCategories.size === 0) {
483
- setExpandedCategories(new Set(categories.map((c) => c.id)))
484
- }
485
- }, [categories]) // eslint-disable-line react-hooks/exhaustive-deps
486
-
487
- // ---- Group items by categoryId ----
488
- const grouped = useMemo(() => {
489
- const map = new Map<string, MenuItem[]>()
490
- for (const item of menuItems) {
491
- const key = item.categoryId ?? '__uncategorized'
492
- const list = map.get(key) ?? []
493
- list.push(item)
494
- map.set(key, list)
495
- }
496
- return map
497
- }, [menuItems])
498
-
499
- // ---- Client-side filtering ----
500
- const filteredCategories = useMemo(() => {
501
- const lowerSearch = search.toLowerCase()
502
-
503
- return categories.filter((cat) => {
504
- // Category dropdown filter
505
- if (categoryFilter !== 'all' && cat.id !== categoryFilter) return false
506
-
507
- // Search filter — match category items by name or description
508
- if (lowerSearch) {
509
- const items = grouped.get(cat.id) ?? []
510
- return items.some(
511
- (item) =>
512
- item.name.toLowerCase().includes(lowerSearch) ||
513
- (item.description?.toLowerCase().includes(lowerSearch) ?? false),
514
- )
515
- }
516
- return true
517
- })
518
- }, [categories, categoryFilter, search, grouped])
519
-
520
- // Get filtered items for a category (applying search within it)
521
- const getFilteredItems = (categoryId: string): MenuItem[] => {
522
- const items = grouped.get(categoryId) ?? []
523
- if (!search) return items
524
- const lowerSearch = search.toLowerCase()
525
- return items.filter(
526
- (item) =>
527
- item.name.toLowerCase().includes(lowerSearch) ||
528
- (item.description?.toLowerCase().includes(lowerSearch) ?? false),
529
- )
530
- }
531
-
532
- const toggleCategory = (id: string) => {
533
- setExpandedCategories((prev) => {
534
- const next = new Set(prev)
535
- next.has(id) ? next.delete(id) : next.add(id)
536
- return next
537
- })
538
- }
539
-
540
- const isLoading = categoriesLoading || menuItemsLoading
541
-
542
- // ---- Loading skeleton ----
543
- if (isLoading && categories.length === 0) {
544
- return (
545
- <div className="space-y-6">
546
- <div className="flex items-center justify-between">
547
- <div>
548
- <div className="h-7 w-40 rounded bg-muted animate-pulse" />
549
- <div className="h-4 w-64 rounded bg-muted animate-pulse mt-2" />
550
- </div>
551
- </div>
552
- <div className="flex items-center gap-3">
553
- <div className="h-9 w-40 rounded bg-muted animate-pulse" />
554
- <div className="h-9 flex-1 max-w-sm rounded bg-muted animate-pulse" />
555
- </div>
556
- <div className="space-y-3">
557
- <CategorySkeleton />
558
- <CategorySkeleton />
559
- <CategorySkeleton />
560
- </div>
561
- </div>
562
- )
563
- }
564
-
565
- return (
566
- <div className="space-y-6">
567
- {/* Header */}
568
- <div className="flex items-center justify-between">
569
- <div>
570
- <h1 className="text-2xl font-bold">{t('menu.manager.title')}</h1>
571
- <p className="text-sm text-muted-foreground">
572
- {t('menu.manager.subtitle')}
573
- </p>
574
- </div>
575
- <div className="flex items-center gap-2">
576
- {isLoading && (
577
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
578
- )}
579
- <button
580
- type="button"
581
- onClick={() => setShowCategoryModal(true)}
582
- className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-muted/50 transition-colors"
583
- >
584
- <Plus className="h-4 w-4" />
585
- {t('menu.manager.addCategory')}
586
- </button>
587
- <button
588
- type="button"
589
- onClick={() => openItemModal(undefined)}
590
- className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
591
- >
592
- <Plus className="h-4 w-4" />
593
- {t('menu.manager.addProduct')}
594
- </button>
595
- </div>
596
- </div>
597
-
598
- {/* Filters */}
599
- <div className="flex items-center gap-3">
600
- <select
601
- value={categoryFilter}
602
- onChange={(e) => setCategoryFilter(e.target.value)}
603
- className="h-9 rounded-md border border-input bg-background px-3 text-sm"
604
- >
605
- <option value="all">{t('menu.manager.allCategories')}</option>
606
- {categories.map((cat) => (
607
- <option key={cat.id} value={cat.id}>
608
- {cat.name}
609
- </option>
610
- ))}
611
- </select>
612
- <div className="relative flex-1 max-w-sm">
613
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
614
- <input
615
- type="text"
616
- value={search}
617
- onChange={(e) => setSearch(e.target.value)}
618
- placeholder={t('menu.manager.search')}
619
- className="h-9 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
620
- />
621
- </div>
622
- </div>
623
-
624
- {/* Categories */}
625
- <div className="space-y-3">
626
- {filteredCategories.map((category) => (
627
- <CategorySection
628
- key={category.id}
629
- category={category}
630
- items={getFilteredItems(category.id)}
631
- expanded={expandedCategories.has(category.id)}
632
- onToggle={() => toggleCategory(category.id)}
633
- onAddProduct={() => openItemModal(category.id)}
634
- />
635
- ))}
636
- </div>
637
-
638
- {/* Empty state — no results after filtering */}
639
- {filteredCategories.length === 0 && !isLoading && (
640
- <div className="text-center py-12">
641
- {categories.length === 0 ? (
642
- <>
643
- <UtensilsCrossed className="mx-auto h-10 w-10 text-muted-foreground/40 mb-3" />
644
- <p className="text-sm font-medium">{t('menu.manager.noItems')}</p>
645
- <p className="text-xs text-muted-foreground mt-1">
646
- {t('menu.manager.noItemsDesc')}
647
- </p>
648
- <div className="mt-4 flex items-center justify-center gap-2">
649
- <button
650
- type="button"
651
- onClick={() => setShowCategoryModal(true)}
652
- className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-muted/50"
653
- >
654
- <Plus className="h-4 w-4" />
655
- {t('menu.manager.addCategory')}
656
- </button>
657
- <button
658
- type="button"
659
- onClick={() => openItemModal(undefined)}
660
- className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
661
- >
662
- <Plus className="h-4 w-4" />
663
- {t('menu.manager.addProduct')}
664
- </button>
665
- </div>
666
- </>
667
- ) : (
668
- <>
669
- <Search className="mx-auto h-10 w-10 text-muted-foreground/40 mb-3" />
670
- <p className="text-sm font-medium">
671
- {t('menu.manager.noResults')}
672
- </p>
673
- <p className="text-xs text-muted-foreground mt-1">
674
- {t('menu.manager.noResultsDesc')}
675
- </p>
676
- </>
677
- )}
678
- </div>
679
- )}
680
-
681
- {showCategoryModal && (
682
- <CategoryModal open onClose={() => setShowCategoryModal(false)} />
683
- )}
684
- {showItemModal && (
685
- <ItemModal
686
- open
687
- categories={categories}
688
- defaultCategoryId={itemModalCategory}
689
- onClose={() => setShowItemModal(false)}
690
- />
691
- )}
692
- </div>
693
- )
694
- }
File without changes