import { apiFetch } from '../api'; import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Plus, MoreVertical, Store, Image as ImageIcon, X, Edit2, Trash2, Check, Loader2, Package, Layers, Search as SearchIcon, Clock, Calendar, AlertCircle } from 'lucide-react'; interface BaseItem { id: number; name: string; } interface Product { id: number; name: string; category: string; price: number; } interface StallSession { id?: number; dayOfWeek: string; active: boolean; startTime: string; endTime: string; } interface Stall { id: number; name: string; description: string; imageData?: string; active: boolean; temporarilyClosed: boolean; sessionOptional: boolean; sessions: StallSession[]; products: Product[]; baseItems: BaseItem[]; } const DAYS = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; const getDefaultSessions = (): StallSession[] => DAYS.map(day => ({ dayOfWeek: day, active: true, startTime: '00:00', endTime: '23:59' })); const Stalls: React.FC = () => { const [stalls, setStalls] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); const [editingStall, setEditingStall] = useState(null); // Form State const [formData, setFormData] = useState({ name: '', description: '', imageData: '' as string | undefined, temporarilyClosed: false, sessionOptional: false, sessions: getDefaultSessions() }); const [isSaving, setIsSaving] = useState(false); const [openMenuId, setOpenMenuId] = useState(null); const [showSessionModal, setShowSessionModal] = useState(false); // Item Linking State const [isItemModalOpen, setIsItemModalOpen] = useState(false); const [selectedStall, setSelectedStall] = useState(null); const [allProducts, setAllProducts] = useState([]); const [allBaseItems, setAllBaseItems] = useState([]); const [tempProductIds, setTempProductIds] = useState([]); const [tempBaseItemIds, setTempBaseItemIds] = useState([]); const [itemSearchQuery, setItemSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState<'products' | 'baseItems'>('products'); useEffect(() => { fetchStalls(); }, []); const fetchStalls = async () => { try { setLoading(true); const host = window.location.hostname; const response = await apiFetch(`http://${host}:8080/api/stalls`); if (response.ok) { const data = await response.json(); setStalls(data); } } catch (error) { console.error('Error fetching stalls:', error); } finally { setLoading(false); } }; const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { setFormData(prev => ({ ...prev, imageData: reader.result as string })); }; reader.readAsDataURL(file); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); try { const host = window.location.hostname; const url = editingStall ? `http://${host}:8080/api/stalls/${editingStall.id}` : `http://${host}:8080/api/stalls`; const response = await apiFetch(url, { method: editingStall ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formData, active: true }) }); if (response.ok) { setIsModalOpen(false); setEditingStall(null); setFormData({ name: '', description: '', imageData: '', temporarilyClosed: false, sessionOptional: false, sessions: getDefaultSessions() }); fetchStalls(); } } catch (error) { console.error('Error saving stall:', error); } finally { setIsSaving(false); } }; const handleEdit = (stall: Stall) => { setOpenMenuId(null); setEditingStall(stall); setFormData({ name: stall.name, description: stall.description, imageData: stall.imageData, temporarilyClosed: stall.temporarilyClosed, sessionOptional: stall.sessionOptional, sessions: stall.sessions && stall.sessions.length > 0 ? stall.sessions : getDefaultSessions() }); setIsModalOpen(true); }; const handleDelete = async (id: number) => { if (!window.confirm('Are you sure you want to delete this stall?')) return; try { const host = window.location.hostname; const response = await apiFetch(`http://${host}:8080/api/stalls/${id}`, { method: 'DELETE' }); if (response.ok) { fetchStalls(); } } catch (error) { console.error('Error deleting stall:', error); } }; const openLinkModal = async (stall: Stall) => { setOpenMenuId(null); setSelectedStall(stall); setTempProductIds(stall.products?.map(p => p.id) || []); setTempBaseItemIds(stall.baseItems?.map(b => b.id) || []); setIsItemModalOpen(true); try { const host = window.location.hostname; const [prodRes, baseRes] = await Promise.all([ apiFetch(`http://${host}:8080/api/products?size=1000`), apiFetch(`http://${host}:8080/api/base-items?size=100`) ]); if (prodRes.ok) { const data = await prodRes.json(); setAllProducts(data.content || data); } if (baseRes.ok) { const data = await baseRes.json(); setAllBaseItems(data.content || data); } } catch (error) { console.error('Error fetching available items:', error); } }; const saveItemAssociations = async () => { if (!selectedStall) return; setIsSaving(true); try { const host = window.location.hostname; const response = await apiFetch(`http://${host}:8080/api/stalls/${selectedStall.id}/items`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productIds: tempProductIds, baseItemIds: tempBaseItemIds }) }); if (response.ok) { setIsItemModalOpen(false); fetchStalls(); } else { const errorData = await response.json(); window.alert(`Failed to save inventory: ${errorData.message || 'Unknown error'}`); } } catch (error) { console.error('Error updating stall items:', error); window.alert('A network error occurred while updating stall inventory.'); } finally { setIsSaving(false); } }; const filteredStalls = stalls.filter(s => s.name.toLowerCase().includes(searchQuery.toLowerCase()) || s.description.toLowerCase().includes(searchQuery.toLowerCase()) ); const isStallOpen = (stall: Stall) => { if (stall.temporarilyClosed) return false; if (!stall.sessionOptional) return true; const now = new Date(); const currentDay = DAYS[now.getDay()]; const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; const session = stall.sessions?.find(s => s.dayOfWeek === currentDay); if (!session || !session.active) return false; return currentTimeStr >= session.startTime && currentTimeStr <= session.endTime; }; const updateSession = (index: number, updates: Partial) => { const newSessions = [...formData.sessions]; newSessions[index] = { ...newSessions[index], ...updates }; setFormData({ ...formData, sessions: newSessions }); }; return (
{/* Header Section */}

Stalls Management

Configure and manage your independent canteen service counters

setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-white border border-[#e2e8f0] rounded-xl text-sm focus:outline-none focus:border-[#231651]/30 transition-all shadow-sm" />
{/* Main Grid Area */}
{loading ? (
{[1, 2, 3, 4].map(i => (
))}
) : filteredStalls.length === 0 ? (

No stalls established

Start by adding your first service counter.

) : (
{filteredStalls.map(stall => (
openLinkModal(stall)} className="group cursor-pointer bg-white rounded-2xl overflow-hidden border border-[#e2e8f0] hover:border-[#231651]/30 transition-all hover:shadow-xl flex flex-col relative" > {/* Image Section */}
{stall.imageData ? ( {stall.name} ) : ( )}
e.stopPropagation()}> {openMenuId === stall.id && ( )}
{/* Content Section */}

{stall.name}

{stall.temporarilyClosed ? 'Closed' : isStallOpen(stall) ? 'Active' : 'Offline'}

{stall.description || 'Dedicated food service counter for our premium canteen offerings.'}

Products {stall.products?.length || 0}
Base Items {stall.baseItems?.length || 0}
))}
)}
{/* Edit Stall Modal - Replicated from Customer Design */} {isModalOpen && (

{editingStall ? 'Edit Stall' : 'Create New Stall'}

Configure your canteen service counter details

setFormData({ ...formData, name: e.target.value })} className="w-full px-5 py-4 bg-slate-50 border border-[#e2e8f0] rounded-2xl text-base font-bold focus:ring-4 focus:ring-[#231651]/5 focus:border-[#231651] transition-all outline-none" placeholder="e.g. Continental Corner" />