import { useState, useEffect, useRef, ReactNode } from 'react' import { adminApi } from '../../../api/client' import { useToast } from '../../../components/shared/Toast' import { useTranslation } from '../../../i18n' import { Plus, Trash2, Edit2, Package, X, Check, ChevronDown, ChevronRight, FolderPlus } from 'lucide-react' import { MAdminButton, MAdminCard, MAdminCardHead, MAdminInput } from './MAdminUi' interface TemplateCategory { id: number; template_id: number; name: string; sort_order: number } interface TemplateItem { id: number; category_id: number; name: string; sort_order: number } interface Template { id: number; name: string; item_count: number; category_count: number; created_by_name: string } // Small round icon action button in the mobile admin idiom (flat --m-ic circle). function PkIconBtn({ onClick, ariaLabel, variant = 'neutral', size = 32, disabled = false, children, }: { onClick: () => void ariaLabel: string variant?: 'neutral' | 'danger' | 'accent' size?: number disabled?: boolean children: ReactNode }) { const look = variant === 'accent' ? 'bg-m-act text-m-actfg' : variant === 'danger' ? 'bg-[color:var(--m-ic)] text-[color:var(--m-st-danger)]' : 'bg-[color:var(--m-ic)] text-m-muted' return ( ) } // Compact inline field matching MAdminInput, used where a native ref is needed // (the add-item input focuses itself after each add). const inlineFieldCls = 'h-[38px] w-full rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-3 text-[0.84375rem] text-m-ink outline-none placeholder:text-m-faint focus:border-[color:var(--m-faint)]' /** * Mobile-native re-skin of the admin Packing Template Manager: create/rename/ * delete templates, expand a template to manage its categories and items. All * state, effects and adminApi mutations are preserved from the desktop version. */ export default function MAdminPackingTemplateManager() { const [templates, setTemplates] = useState([]) const [isLoading, setIsLoading] = useState(true) const [showCreate, setShowCreate] = useState(false) const [createName, setCreateName] = useState('') // Expanded template state const [expandedId, setExpandedId] = useState(null) const [categories, setCategories] = useState([]) const [items, setItems] = useState([]) // Editing states const [editingTemplate, setEditingTemplate] = useState(null) const [editTemplateName, setEditTemplateName] = useState('') const [editingCatId, setEditingCatId] = useState(null) const [editCatName, setEditCatName] = useState('') const [editingItemId, setEditingItemId] = useState(null) const [editItemName, setEditItemName] = useState('') // Adding states const [addingCategory, setAddingCategory] = useState(false) const [newCatName, setNewCatName] = useState('') const [addingItemToCatId, setAddingItemToCatId] = useState(null) const [newItemName, setNewItemName] = useState('') const addItemRef = useRef(null) const toast = useToast() const { t } = useTranslation() useEffect(() => { loadTemplates() }, []) const loadTemplates = async () => { setIsLoading(true) try { const data = await adminApi.packingTemplates() setTemplates(data.templates || []) } catch { toast.error(t('admin.packingTemplates.loadError')) } finally { setIsLoading(false) } } const toggleExpand = async (id: number) => { if (expandedId === id) { setExpandedId(null); return } setExpandedId(id) setAddingCategory(false) setAddingItemToCatId(null) try { const data = await adminApi.getPackingTemplate(id) setCategories(data.categories || []) setItems(data.items || []) } catch { toast.error(t('admin.packingTemplates.loadError')) } } // Template CRUD const handleCreateTemplate = async () => { if (!createName.trim()) return try { const data = await adminApi.createPackingTemplate({ name: createName.trim() }) setTemplates(prev => [{ ...data.template, item_count: 0, category_count: 0 }, ...prev]) setCreateName(''); setShowCreate(false) setExpandedId(data.template.id); setCategories([]); setItems([]) toast.success(t('admin.packingTemplates.created')) } catch { toast.error(t('admin.packingTemplates.createError')) } } const handleDeleteTemplate = async (id: number) => { try { await adminApi.deletePackingTemplate(id) setTemplates(prev => prev.filter(t => t.id !== id)) if (expandedId === id) setExpandedId(null) toast.success(t('admin.packingTemplates.deleted')) } catch { toast.error(t('admin.packingTemplates.deleteError')) } } const handleRenameTemplate = async (id: number) => { if (!editTemplateName.trim()) { setEditingTemplate(null); return } try { await adminApi.updatePackingTemplate(id, { name: editTemplateName.trim() }) setTemplates(prev => prev.map(t => t.id === id ? { ...t, name: editTemplateName.trim() } : t)) setEditingTemplate(null) } catch { toast.error(t('admin.packingTemplates.saveError')) } } // Category CRUD const handleAddCategory = async () => { if (!newCatName.trim() || !expandedId) return try { const data = await adminApi.addTemplateCategory(expandedId, { name: newCatName.trim() }) setCategories(prev => [...prev, data.category]) setNewCatName(''); setAddingCategory(false) } catch { toast.error(t('admin.packingTemplates.saveError')) } } const handleRenameCategory = async (catId: number) => { if (!editCatName.trim() || !expandedId) { setEditingCatId(null); return } try { await adminApi.updateTemplateCategory(expandedId, catId, { name: editCatName.trim() }) setCategories(prev => prev.map(c => c.id === catId ? { ...c, name: editCatName.trim() } : c)) setEditingCatId(null) } catch { toast.error(t('admin.packingTemplates.saveError')) } } const handleDeleteCategory = async (catId: number) => { if (!expandedId) return try { await adminApi.deleteTemplateCategory(expandedId, catId) setCategories(prev => prev.filter(c => c.id !== catId)) setItems(prev => prev.filter(i => i.category_id !== catId)) } catch { toast.error(t('admin.toast.deleteError')) } } // Item CRUD const handleAddItem = async (catId: number) => { if (!newItemName.trim() || !expandedId) return try { const data = await adminApi.addTemplateItem(expandedId, catId, { name: newItemName.trim() }) setItems(prev => [...prev, data.item]) setNewItemName('') setTimeout(() => addItemRef.current?.focus(), 30) } catch { toast.error(t('admin.packingTemplates.saveError')) } } const handleRenameItem = async (itemId: number) => { if (!editItemName.trim() || !expandedId) { setEditingItemId(null); return } try { await adminApi.updateTemplateItem(expandedId, itemId, { name: editItemName.trim() }) setItems(prev => prev.map(i => i.id === itemId ? { ...i, name: editItemName.trim() } : i)) setEditingItemId(null) } catch { toast.error(t('admin.packingTemplates.saveError')) } } const handleDeleteItem = async (itemId: number) => { if (!expandedId) return try { await adminApi.deleteTemplateItem(expandedId, itemId) setItems(prev => prev.filter(i => i.id !== itemId)) } catch { toast.error(t('admin.toast.deleteError')) } } return ( {/* Header */} setShowCreate(true)}> {t('admin.packingTemplates.create')} } /> {/* Create template */} {showCreate && (
setCreateName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreateTemplate(); if (e.key === 'Escape') setShowCreate(false) }} placeholder={t('admin.packingTemplates.namePlaceholder')} />
setShowCreate(false)}>
)} {/* Template list */} {isLoading ? (
) : templates.length === 0 ? (
{t('admin.packingTemplates.empty')}
) : (
{templates.map(tmpl => (
{/* Template row */}
{editingTemplate === tmpl.id ? (
setEditTemplateName(e.target.value)} onBlur={() => handleRenameTemplate(tmpl.id)} onKeyDown={e => { if (e.key === 'Enter') handleRenameTemplate(tmpl.id); if (e.key === 'Escape') setEditingTemplate(null) }} />
) : ( )} { setEditingTemplate(tmpl.id); setEditTemplateName(tmpl.name) }} > handleDeleteTemplate(tmpl.id)} >
{/* Expanded content */} {expandedId === tmpl.id && (
{categories.map(cat => { const catItems = items.filter(i => i.category_id === cat.id) return (
{/* Category header */}
{editingCatId === cat.id ? (
setEditCatName(e.target.value)} onBlur={() => handleRenameCategory(cat.id)} onKeyDown={e => { if (e.key === 'Enter') handleRenameCategory(cat.id); if (e.key === 'Escape') setEditingCatId(null) }} />
) : ( {cat.name} )} {catItems.length} { setAddingItemToCatId(addingItemToCatId === cat.id ? null : cat.id); setNewItemName(''); setTimeout(() => addItemRef.current?.focus(), 30) }} > { setEditingCatId(cat.id); setEditCatName(cat.name) }} > handleDeleteCategory(cat.id)} >
{/* Items */} {(catItems.length > 0 || addingItemToCatId === cat.id) && (
{catItems.map(item => (
{editingItemId === item.id ? ( <>
setEditItemName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleRenameItem(item.id); if (e.key === 'Escape') setEditingItemId(null) }} />
handleRenameItem(item.id)}> setEditingItemId(null)}> ) : ( <> {item.name} { setEditingItemId(item.id); setEditItemName(item.name) }} > handleDeleteItem(item.id)} > )}
))} {/* Add item inline */} {addingItemToCatId === cat.id && (
setNewItemName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newItemName.trim()) handleAddItem(cat.id); if (e.key === 'Escape') { setAddingItemToCatId(null); setNewItemName('') } }} placeholder={t('admin.packingTemplates.itemName')} className={inlineFieldCls} /> handleAddItem(cat.id)}> { setAddingItemToCatId(null); setNewItemName('') }}>
)}
)}
) })} {/* Add category */} {addingCategory ? (
setNewCatName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleAddCategory(); if (e.key === 'Escape') { setAddingCategory(false); setNewCatName('') } }} placeholder={t('admin.packingTemplates.categoryName')} />
{ setAddingCategory(false); setNewCatName('') }}>
) : ( )}
)}
))}
)}
) }