import { useState, useEffect, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { MapPin, Plus, Loader2, Pencil, Trash2, X } from 'lucide-react'; import { api, type StorageLocation } from '../api/client'; import { Button } from './Button'; import { ConfirmModal } from './ConfirmModal'; import { useToast } from '../contexts/ToastContext'; import { inventoryLocationsQueryKey, invalidateInventoryLocations } from '../utils/inventoryQueries'; interface LocationsModalProps { open: boolean; onClose: () => void; onPickLocation?: (locationId: number) => void; } export function LocationsModal({ open, onClose, onPickLocation }: LocationsModalProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [editorOpen, setEditorOpen] = useState(false); const [editing, setEditing] = useState(null); const [name, setName] = useState(''); const [deleteTarget, setDeleteTarget] = useState(null); const { data: locations = [], isLoading } = useQuery({ queryKey: inventoryLocationsQueryKey, queryFn: api.getLocations, enabled: open, }); const invalidate = () => { invalidateInventoryLocations(queryClient); queryClient.invalidateQueries({ queryKey: ['inventory-spools'] }); queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] }); }; const saveMutation = useMutation({ mutationFn: async () => { const trimmed = name.trim(); if (!trimmed) throw new Error(t('locations.nameRequired')); if (editing) { return api.updateLocation(editing.id, { name: trimmed }); } return api.createLocation({ name: trimmed }); }, onSuccess: () => { showToast(t(editing ? 'locations.updated' : 'locations.created'), 'success'); setEditorOpen(false); setEditing(null); setName(''); invalidate(); }, onError: (err: Error) => { showToast(err.message || t('locations.saveFailed'), 'error'); }, }); const deleteMutation = useMutation({ mutationFn: (id: number) => api.deleteLocation(id), onSuccess: () => { showToast(t('locations.deleted'), 'success'); setDeleteTarget(null); invalidate(); }, onError: (err: Error) => { showToast(err.message || t('locations.deleteFailed'), 'error'); }, }); const openCreate = () => { setEditing(null); setName(''); setEditorOpen(true); }; const openEdit = (location: StorageLocation) => { setEditing(location); setName(location.name); setEditorOpen(true); }; const closeEditor = useCallback(() => { if (saveMutation.isPending) return; setEditorOpen(false); setEditing(null); setName(''); }, [saveMutation.isPending]); // Esc closes the inner editor first; if it's closed, Esc closes the outer // modal — but only when neither save nor delete is mid-flight, so a stray // keypress during a network round-trip doesn't drop the user back into the // inventory page with an orphaned spinner. useEffect(() => { if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key !== 'Escape') return; if (saveMutation.isPending || deleteMutation.isPending) return; if (editorOpen) { closeEditor(); } else if (!deleteTarget) { onClose(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [open, editorOpen, deleteTarget, saveMutation.isPending, deleteMutation.isPending, closeEditor, onClose]); const handleSave = (e: React.FormEvent) => { e.preventDefault(); saveMutation.mutate(); }; if (!open) return null; const modalTitleId = 'locations-modal-title'; const editorTitleId = 'location-editor-title'; return (
{ if (saveMutation.isPending || deleteMutation.isPending) return; onClose(); }} />

{t('locations.title')}

{t('locations.subtitle')}

{isLoading ? (
{t('common.loading')}
) : locations.length === 0 ? (
{t('locations.empty')}
) : ( {locations.map((loc) => ( { if (onPickLocation) { onPickLocation(loc.id); onClose(); } }} > ))}
{t('locations.name')} {t('locations.spools')} {t('common.actions')}
{loc.name} {loc.spool_count} e.stopPropagation()}>
)}
{editorOpen && (

{editing ? t('locations.edit') : t('locations.add')}

setName(e.target.value)} autoFocus />
)} {deleteTarget && ( deleteMutation.mutate(deleteTarget.id)} onCancel={() => setDeleteTarget(null)} /> )}
); }