diff --git a/frontend-vite/react-ts/e2e/generation-flow.spec.ts b/frontend-vite/react-ts/e2e/generation-flow.spec.ts index 11b0c4b..e2920b8 100644 --- a/frontend-vite/react-ts/e2e/generation-flow.spec.ts +++ b/frontend-vite/react-ts/e2e/generation-flow.spec.ts @@ -46,11 +46,3 @@ test('file selection is cleared after successful generation', async ({ page }) = ]); await expect(page.getByRole('button', { name: 'Browse Files' })).toBeVisible(); }); - -test('sheet history refreshes after successful generation', async ({ page }) => { - await uploadFile(page); - await page.getByRole('button', { name: 'Generate Label Sheets' }).click(); - // After upload, my-sheets is fetched again — wait for that second request - await page.waitForResponse('**/my-sheets'); - await expect(page.getByText('inventory.csv')).toBeVisible(); -}); diff --git a/frontend-vite/react-ts/e2e/sheet-history.spec.ts b/frontend-vite/react-ts/e2e/sheet-history.spec.ts index c1c0e9b..eec3495 100644 --- a/frontend-vite/react-ts/e2e/sheet-history.spec.ts +++ b/frontend-vite/react-ts/e2e/sheet-history.spec.ts @@ -3,7 +3,7 @@ import { setupApiMocks } from './fixtures/api-mocks'; test.beforeEach(async ({ page }) => { await setupApiMocks(page); - await page.goto('/upload'); + await page.goto('/inventory'); }); test('sheet history table renders rows from GET /my-sheets', async ({ page }) => { diff --git a/frontend-vite/react-ts/src/App.tsx b/frontend-vite/react-ts/src/App.tsx index fa60546..717b0b0 100644 --- a/frontend-vite/react-ts/src/App.tsx +++ b/frontend-vite/react-ts/src/App.tsx @@ -7,6 +7,7 @@ import ProtectedRoute from './config/ProtectedRoute'; import Help from './components/help'; import SignUp from './components/signup'; import LandingPage from './pages/LandingPage'; +import Inventory from './components/Inventory'; function UploadPage() { return ( @@ -19,6 +20,17 @@ function UploadPage() { ); } +function InventoryPage() { + return ( +
+ +
+ +
+
+ ); +} + function App() { return ( @@ -41,6 +53,14 @@ function App() { } /> + + + + } + /> } /> } /> diff --git a/frontend-vite/react-ts/src/components/Inventory.tsx b/frontend-vite/react-ts/src/components/Inventory.tsx new file mode 100644 index 0000000..b17729a --- /dev/null +++ b/frontend-vite/react-ts/src/components/Inventory.tsx @@ -0,0 +1,538 @@ +import { useState, useEffect, useRef } from 'react'; +import axios from 'axios'; +import { + Download, Trash2, RefreshCw, FileSpreadsheet, + AlertCircle, CheckCircle2, + ChevronDown, ChevronRight, Search, Printer, +} from 'lucide-react'; + +interface UserSheet { + id: string; + original_filename: string; + label_count: number; + sheet_count: number; + total_size_bytes: number; + created_at: string; + template_id?: string; + barcode_type?: string; +} + +interface LabelItem { + id: string; + user_sheet_id: string; + original_filename: string; + label_index: number; + sheet_number: number; + position_on_sheet: number; + barcode_value: string; + text_fields: { label: string; value: string }[]; + barcode_type: string; + template_id: string; + created_at: string; +} + +const templates = [ + { id: 'standard_20', rows: 5, cols: 4 }, + { id: '5163', rows: 5, cols: 2 }, + { id: '5160', rows: 10, cols: 3 }, + { id: '94233', rows: 4, cols: 3 }, +]; + +function Inventory() { + // Sheets + const [userSheets, setUserSheets] = useState([]); + const [fetchingSheets, setFetchingSheets] = useState(true); + const [filterFilename, setFilterFilename] = useState(''); + const [expandedSheetId, setExpandedSheetId] = useState(null); + const [selectedSheets, setSelectedSheets] = useState<{ [sheetId: string]: Set }>({}); + const [sheetStates, setSheetStates] = useState<{ + [id: string]: { downloadStatus: 'idle' | 'downloading' | 'downloaded'; deleteStatus: 'idle' | 'deleting' }; + }>({}); + const [singleSheetDownloadStates, setSingleSheetDownloadStates] = useState<{ [key: string]: 'idle' | 'downloading' }>({}); + const [selectedDownloadStates, setSelectedDownloadStates] = useState<{ [id: string]: 'idle' | 'downloading' }>({}); + + // Search + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [searchTotal, setSearchTotal] = useState(0); + const [searchLoading, setSearchLoading] = useState(false); + const [searchOffset, setSearchOffset] = useState(0); + const [reprintStates, setReprintStates] = useState<{ [id: string]: 'idle' | 'downloading' }>({}); + const searchTimeoutRef = useRef | null>(null); + + // Feedback + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000'; + const SEARCH_LIMIT = 20; + + // Handler stubs + const fetchUserSheets = async () => { + try { + const response = await axios.get(`${API_URL}/my-sheets`, { withCredentials: true }); + setUserSheets(response.data.sheets); + const currentIds = new Set(response.data.sheets.map((s: UserSheet) => s.id)); + setSheetStates(prev => Object.fromEntries(Object.entries(prev).filter(([id]) => currentIds.has(id)))); + } catch (err) { + console.error('Error fetching sheets:', err); + setError('Failed to load your saved sheets'); + } finally { + setFetchingSheets(false); + } + }; + + const triggerBlobDownload = (_data: BlobPart, _type: string, _filename: string) => { + const blob = new Blob([_data], { type: _type }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = _filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }; + + const downloadSheet = async (_userSheetId: string, _filename: string) => { + setSheetStates(prev => ({ ...prev, [_userSheetId]: { ...prev[_userSheetId], downloadStatus: 'downloading' } })); + try { + const response = await axios.get(`${API_URL}/download-sheet/${_userSheetId}`, { + withCredentials: true, responseType: 'blob', + }); + triggerBlobDownload(response.data, 'application/zip', `${_filename.split('.')[0]}_labels.zip`); + setSheetStates(prev => ({ ...prev, [_userSheetId]: { ...prev[_userSheetId], downloadStatus: 'downloaded' } })); + } catch { + setError('Failed to download sheet'); + setTimeout(() => setError(''), 5000); + setSheetStates(prev => ({ ...prev, [_userSheetId]: { ...prev[_userSheetId], downloadStatus: 'idle' } })); + } + }; + + const downloadSingleSheet = async (_userSheetId: string, _sheetNumber: number, _filename: string) => { + const key = `${_userSheetId}_${_sheetNumber}`; + setSingleSheetDownloadStates(prev => ({ ...prev, [key]: 'downloading' })); + + try { + const response = await axios.get( + `${API_URL}/download-sheet/${_userSheetId}/sheet/${_sheetNumber}`, + { withCredentials: true, responseType: 'blob' }, + ); + triggerBlobDownload(response.data, 'application/pdf', `${_filename.split('.')[0]}_sheet_${_sheetNumber}.pdf`); + } catch { + setError('Failed to download sheet'); + setTimeout(() => setError(''), 5000); + } finally { + setSingleSheetDownloadStates(prev => ({ ...prev, [key]: 'idle' })); + } + + }; + + const downloadSelectedSheets = async (_userSheetId: string, _filename: string) => { + const selected = selectedSheets[_userSheetId]; + if (!selected || selected.size === 0) return; + setSelectedDownloadStates(prev => ({ ...prev, [_userSheetId]: 'downloading' })); + try { + const response = await axios.post( + `${API_URL}/download-sheet/${_userSheetId}/selected`, + { sheet_numbers: Array.from(selected) }, + { withCredentials: true, responseType: 'blob' }, + ); + triggerBlobDownload(response.data, 'application/zip', `${_filename.split('.')[0]}_selected_sheets.zip`); + } catch { + setError('Failed to download selected sheets'); + setTimeout(() => setError(''), 5000); + } finally { + setSelectedDownloadStates(prev => ({ ...prev, [_userSheetId]: 'idle' })); + } + }; + + const deleteSheet = async (_userSheetId: string) => { + if (!confirm('Are you sure you want to delete this sheet? This cannot be undone.')) return; + setSheetStates(prev => ({ ...prev, [_userSheetId]: { ...prev[_userSheetId], deleteStatus: 'deleting' } })); + try { + await axios.delete(`${API_URL}/delete-sheet/${_userSheetId}`, { withCredentials: true }); + setUserSheets(userSheets.filter(s => s.id !== _userSheetId)); + setSheetStates(prev => { const n = { ...prev }; delete n[_userSheetId]; return n; }); + if (expandedSheetId === _userSheetId) setExpandedSheetId(null); + } catch { + setError('Failed to delete sheet'); + setTimeout(() => setError(''), 5000); + setSheetStates(prev => ({ ...prev, [_userSheetId]: { ...prev[_userSheetId], deleteStatus: 'idle' } })); + } + }; + + const toggleExpand = (_sheetId: string) => { + if (expandedSheetId === _sheetId) { + setExpandedSheetId(null); + setSelectedSheets(prev => { const n = { ...prev }; delete n[_sheetId]; return n; }); + } else { + setExpandedSheetId(_sheetId); + } + }; + + const toggleSheetSelection = (_sheetId: string, _sheetNum: number) => { + setSelectedSheets(prev => { + const current = new Set(prev[_sheetId] || []); + current.has(_sheetNum) ? current.delete(_sheetNum) : current.add(_sheetNum); + return { ...prev, [_sheetId]: current }; + }); + }; + + const getLabelRange = (_sheet: UserSheet, _sheetNum: number): string => { + if (!_sheet.template_id) return ''; + const tmpl = templates.find(t => t.id === _sheet.template_id); + if (!tmpl) return ''; + const lps = tmpl.rows * tmpl.cols; + const start = (_sheetNum - 1) * lps + 1; + const end = Math.min(_sheetNum * lps, _sheet.label_count); + return `labels ${start}–${end}`; + }; + + const handleSearchInput = (_value: string) => { + setSearchQuery(_value); + if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); + if (!_value.trim()) { setSearchResults([]); setSearchTotal(0); return; } + searchTimeoutRef.current = setTimeout(() => searchLabels(_value.trim(), 0), 300); + }; + + const searchLabels = async (_query: string, _offset: number) => { + if (!_query.trim()) { setSearchResults([]); setSearchTotal(0); return; } + setSearchLoading(true); + try { + const response = await axios.get(`${API_URL}/labels/search`, { + params: { q: _query, limit: SEARCH_LIMIT, _offset }, + withCredentials: true, + }); + if (_offset === 0) { + setSearchResults(response.data.results); + } else { + setSearchResults(prev => [...prev, ...response.data.results]); + } + setSearchTotal(response.data.total); + setSearchOffset(_offset); + } catch { + setError('Label search failed'); + setTimeout(() => setError(''), 5000); + } finally { + setSearchLoading(false); + } + }; + + const reprintLabel = async (_labelId: string) => { + setReprintStates(prev => ({ ...prev, [_labelId]: 'downloading' })); + try { + const response = await axios.get(`${API_URL}/labels/${_labelId}/reprint`, { + withCredentials: true, responseType: 'blob', + }); + triggerBlobDownload(response.data, 'application/pdf', `reprint_${_labelId.slice(0, 8)}.pdf`); + } catch { + setError('Failed to generate reprint'); + setTimeout(() => setError(''), 5000); + } finally { + setReprintStates(prev => ({ ...prev, [_labelId]: 'idle' })); + } + }; + + useEffect(() => { fetchUserSheets(); }, []); + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + }; + + const formatDate = (d: string) => new Date(d).toLocaleString(); + + const filteredSheets = filterFilename + ? userSheets.filter(s => s.original_filename.toLowerCase().includes(filterFilename.toLowerCase())) + : userSheets; + + // Suppress unused-variable warnings on stubs until user implements them + void setUserSheets; void setFetchingSheets; void setExpandedSheetId; + void setSelectedSheets; void setSheetStates; void setSingleSheetDownloadStates; + void setSelectedDownloadStates; void setSearchResults; void setSearchTotal; + void setSearchLoading; void setSearchOffset; void setReprintStates; + void setError; void setSuccess; void searchTimeoutRef; void triggerBlobDownload; + void templates; + + return ( +
+ {/* Page Header */} +
+

Inventory

+

Manage and search your generated label sheets

+
+ + {/* Error / Success banners */} + {error && ( +
+ +

{error}

+
+ )} + {success && ( +
+ +

{success}

+
+ )} + + {/* Label Search Card */} +
+
+

Label Search

+

Find and reprint any label by barcode value

+
+
+
+ + handleSearchInput(e.target.value)} + className="h-10 w-full pl-10 pr-4 rounded-[10px] border border-border bg-card text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary placeholder:text-muted-foreground/50" + /> +
+ + {searchLoading && ( +
+
+ Searching... +
+ )} + + {!searchLoading && searchQuery && searchResults.length === 0 && ( +

No labels found for "{searchQuery}"

+ )} + + {searchResults.length > 0 && ( +
+ {searchResults.map(item => ( +
+
+

{item.barcode_value}

+

+ {item.text_fields.map(tf => `${tf.label}: ${tf.value}`).join(' · ')} + {item.text_fields.length > 0 && ' · '} + {item.original_filename} · Sheet {item.sheet_number} +

+
+ +
+ ))} + + {searchResults.length < searchTotal && ( +
+ +
+ )} + +

+ Showing {searchResults.length} of {searchTotal} result{searchTotal !== 1 ? 's' : ''} +

+
+ )} +
+
+ + {/* Sheet History Card */} +
+
+
+

Sheet History

+

+ {fetchingSheets ? 'Loading...' : filterFilename + ? `${filteredSheets.length} of ${userSheets.length} sheet${userSheets.length !== 1 ? 's' : ''}` + : `${userSheets.length} generated sheet${userSheets.length !== 1 ? 's' : ''}`} +

+
+
+
+ + setFilterFilename(e.target.value)} + className="h-9 w-full pl-8 pr-3 rounded-[10px] border border-border bg-card text-foreground text-xs focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary placeholder:text-muted-foreground/50" + /> +
+ +
+
+ + {fetchingSheets ? ( +
+
+

Loading your sheets...

+
+ ) : filteredSheets.length > 0 ? ( +
10 ? 'max-h-[600px] overflow-y-auto' : ''}> + + + + + + + + + + + + + {filteredSheets.map((sheet) => { + const downloadStatus = sheetStates[sheet.id]?.downloadStatus || 'idle'; + const deleteStatus = sheetStates[sheet.id]?.deleteStatus || 'idle'; + const isExpanded = expandedSheetId === sheet.id; + const numSelected = selectedSheets[sheet.id]?.size ?? 0; + + return ( + <> + + + + + + + + + + + {isExpanded && ( + + + + )} + + ); + })} + +
+ FilenameLabelsSheetsSizeCreatedActions
+ + +
+ + {sheet.original_filename} +
+
{sheet.label_count}{sheet.sheet_count}{formatFileSize(sheet.total_size_bytes)}{formatDate(sheet.created_at)} +
+ + +
+
+
+
+ {Array.from({ length: sheet.sheet_count }, (_, i) => i + 1).map(sheetNum => { + const key = `${sheet.id}_${sheetNum}`; + const isDownloading = singleSheetDownloadStates[key] === 'downloading'; + const isSelected = selectedSheets[sheet.id]?.has(sheetNum) ?? false; + const range = getLabelRange(sheet, sheetNum); + return ( +
+ toggleSheetSelection(sheet.id, sheetNum)} + className="w-3.5 h-3.5 rounded border-border accent-primary cursor-pointer" + /> + + Sheet {sheetNum} + {range && {range}} + + +
+ ); + })} +
+ + {numSelected > 0 && ( +
+ {numSelected} sheet{numSelected !== 1 ? 's' : ''} selected + +
+ )} +
+
+
+ ) : ( +
+
+ +
+

+ {filterFilename ? 'No sheets match your filter.' : 'No sheets generated yet. Upload a file to get started.'} +

+
+ )} +
+
+ ); +} + +export default Inventory; diff --git a/frontend-vite/react-ts/src/components/Sidebar.tsx b/frontend-vite/react-ts/src/components/Sidebar.tsx index 1f291bf..266dcd2 100644 --- a/frontend-vite/react-ts/src/components/Sidebar.tsx +++ b/frontend-vite/react-ts/src/components/Sidebar.tsx @@ -5,6 +5,7 @@ import { ScanBarcode, LayoutDashboard, Upload, + Package, HelpCircle, LogOut, User, @@ -66,6 +67,7 @@ function Sidebar({ activeOverride }: SidebarProps) { const navItems = [ { label: 'Dashboard', icon: LayoutDashboard, path: '/dashboard' }, { label: 'Upload Labels', icon: Upload, path: '/upload' }, + { label: 'Inventory', icon: Package, path: '/inventory' }, { label: 'Help', icon: HelpCircle, path: '/help' }, ]; diff --git a/frontend-vite/react-ts/src/components/labelUploader.tsx b/frontend-vite/react-ts/src/components/labelUploader.tsx index 3ef7e2c..98755e4 100644 --- a/frontend-vite/react-ts/src/components/labelUploader.tsx +++ b/frontend-vite/react-ts/src/components/labelUploader.tsx @@ -1,10 +1,8 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useRef } from 'react'; import type { ChangeEvent } from 'react'; import axios from 'axios'; import { - Upload, Download, Trash2, RefreshCw, FileSpreadsheet, - AlertCircle, CheckCircle2, Plus, X, - ChevronDown, ChevronRight, Search, Printer, + Upload, AlertCircle, CheckCircle2, Plus, X, } from 'lucide-react'; import LabelPreview from './LabelPreview'; @@ -18,31 +16,6 @@ interface BackendUploadResponse { message: string; } -interface UserSheet { - id: string; - original_filename: string; - label_count: number; - sheet_count: number; - total_size_bytes: number; - created_at: string; - template_id?: string; - barcode_type?: string; -} - -interface LabelItem { - id: string; - user_sheet_id: string; - original_filename: string; - label_index: number; - sheet_number: number; - position_on_sheet: number; - barcode_value: string; - text_fields: { label: string; value: string }[]; - barcode_type: string; - template_id: string; - created_at: string; -} - interface PreviewData { preview_pdf: string; label_count: number; @@ -52,9 +25,7 @@ interface PreviewData { function LabelUploader() { const [file, setFile] = useState(null); - const [userSheets, setUserSheets] = useState([]); const [loading, setLoading] = useState(false); - const [fetchingSheets, setFetchingSheets] = useState(true); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const fileInputRef = useRef(null); @@ -72,25 +43,6 @@ function LabelUploader() { const [previewing, setPreviewing] = useState(false); const [previewData, setPreviewData] = useState(null); - // Sheet expansion + selective download state - const [expandedSheetId, setExpandedSheetId] = useState(null); - const [selectedSheets, setSelectedSheets] = useState<{ [sheetId: string]: Set }>({}); - const [singleSheetDownloadStates, setSingleSheetDownloadStates] = useState<{ [key: string]: 'idle' | 'downloading' }>({}); - const [selectedDownloadStates, setSelectedDownloadStates] = useState<{ [sheetId: string]: 'idle' | 'downloading' }>({}); - - // History filter - const [filterFilename, setFilterFilename] = useState(''); - - // Label search state - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [searchTotal, setSearchTotal] = useState(0); - const [searchLoading, setSearchLoading] = useState(false); - const [searchOffset, setSearchOffset] = useState(0); - const [reprintStates, setReprintStates] = useState<{ [labelId: string]: 'idle' | 'downloading' }>({}); - const searchTimeoutRef = useRef | null>(null); - const SEARCH_LIMIT = 50; - const templates = [ { id: 'standard_20', name: 'Standard', desc: '20 labels per sheet', size: '1.75" x 1.8"', grid: '5 x 4', rows: 5, cols: 4, maxTextLines: 2 }, { id: '5163', name: '5163', desc: 'Compatible with Avery 5163', size: '2" x 4"', grid: '5 x 2', rows: 5, cols: 2, maxTextLines: 2 }, @@ -121,200 +73,6 @@ function LabelUploader() { const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000'; - const [sheetStates, setSheetStates] = useState<{ - [sheetId: string]: { - downloadStatus: 'idle' | 'downloading' | 'downloaded'; - deleteStatus: 'idle' | 'deleting'; - } - }>(() => { - const saved = localStorage.getItem('sheetStates'); - if (saved && saved !== 'undefined') { - try { return JSON.parse(saved); } catch { return {}; } - } - return {}; - }); - - useEffect(() => { fetchUserSheets(); }, []); - - useEffect(() => { - localStorage.setItem('sheetStates', JSON.stringify(sheetStates)); - }, [sheetStates]); - - const fetchUserSheets = async () => { - try { - const response = await axios.get(`${API_URL}/my-sheets`, { withCredentials: true }); - setUserSheets(response.data.sheets); - const currentIds = new Set(response.data.sheets.map((s: UserSheet) => s.id)); - setSheetStates(prev => Object.fromEntries(Object.entries(prev).filter(([id]) => currentIds.has(id)))); - } catch (err) { - console.error('Error fetching sheets:', err); - setError('Failed to load your saved sheets'); - } finally { - setFetchingSheets(false); - } - }; - - // ── Blob download helper ────────────────────────────────────────────── - const triggerBlobDownload = (data: BlobPart, type: string, filename: string) => { - const blob = new Blob([data], { type }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - }; - - // ── Full ZIP download ──────────────────────────────────────────────── - const downloadSheet = async (userSheetId: string, filename: string) => { - setSheetStates(prev => ({ ...prev, [userSheetId]: { ...prev[userSheetId], downloadStatus: 'downloading' } })); - try { - const response = await axios.get(`${API_URL}/download-sheet/${userSheetId}`, { - withCredentials: true, responseType: 'blob', - }); - triggerBlobDownload(response.data, 'application/zip', `${filename.split('.')[0]}_labels.zip`); - setSheetStates(prev => ({ ...prev, [userSheetId]: { ...prev[userSheetId], downloadStatus: 'downloaded' } })); - } catch { - setError('Failed to download sheet'); - setTimeout(() => setError(''), 5000); - setSheetStates(prev => ({ ...prev, [userSheetId]: { ...prev[userSheetId], downloadStatus: 'idle' } })); - } - }; - - // ── Single sheet download ──────────────────────────────────────────── - const downloadSingleSheet = async (userSheetId: string, sheetNumber: number, filename: string) => { - const key = `${userSheetId}_${sheetNumber}`; - setSingleSheetDownloadStates(prev => ({ ...prev, [key]: 'downloading' })); - try { - const response = await axios.get( - `${API_URL}/download-sheet/${userSheetId}/sheet/${sheetNumber}`, - { withCredentials: true, responseType: 'blob' }, - ); - triggerBlobDownload(response.data, 'application/pdf', `${filename.split('.')[0]}_sheet_${sheetNumber}.pdf`); - } catch { - setError('Failed to download sheet'); - setTimeout(() => setError(''), 5000); - } finally { - setSingleSheetDownloadStates(prev => ({ ...prev, [key]: 'idle' })); - } - }; - - // ── Selected sheets download ───────────────────────────────────────── - const downloadSelectedSheets = async (userSheetId: string, filename: string) => { - const selected = selectedSheets[userSheetId]; - if (!selected || selected.size === 0) return; - setSelectedDownloadStates(prev => ({ ...prev, [userSheetId]: 'downloading' })); - try { - const response = await axios.post( - `${API_URL}/download-sheet/${userSheetId}/selected`, - { sheet_numbers: Array.from(selected) }, - { withCredentials: true, responseType: 'blob' }, - ); - triggerBlobDownload(response.data, 'application/zip', `${filename.split('.')[0]}_selected_sheets.zip`); - } catch { - setError('Failed to download selected sheets'); - setTimeout(() => setError(''), 5000); - } finally { - setSelectedDownloadStates(prev => ({ ...prev, [userSheetId]: 'idle' })); - } - }; - - // ── Expand / collapse ──────────────────────────────────────────────── - const toggleExpand = (sheetId: string) => { - if (expandedSheetId === sheetId) { - setExpandedSheetId(null); - setSelectedSheets(prev => { const n = { ...prev }; delete n[sheetId]; return n; }); - } else { - setExpandedSheetId(sheetId); - } - }; - - const toggleSheetSelection = (sheetId: string, sheetNum: number) => { - setSelectedSheets(prev => { - const current = new Set(prev[sheetId] || []); - current.has(sheetNum) ? current.delete(sheetNum) : current.add(sheetNum); - return { ...prev, [sheetId]: current }; - }); - }; - - const getLabelRange = (sheet: UserSheet, sheetNum: number): string => { - if (!sheet.template_id) return ''; - const tmpl = templates.find(t => t.id === sheet.template_id); - if (!tmpl) return ''; - const lps = tmpl.rows * tmpl.cols; - const start = (sheetNum - 1) * lps + 1; - const end = Math.min(sheetNum * lps, sheet.label_count); - return `labels ${start}–${end}`; - }; - - // ── Label search ───────────────────────────────────────────────────── - const searchLabels = async (query: string, offset = 0) => { - if (!query.trim()) { setSearchResults([]); setSearchTotal(0); return; } - setSearchLoading(true); - try { - const response = await axios.get(`${API_URL}/labels/search`, { - params: { q: query, limit: SEARCH_LIMIT, offset }, - withCredentials: true, - }); - if (offset === 0) { - setSearchResults(response.data.results); - } else { - setSearchResults(prev => [...prev, ...response.data.results]); - } - setSearchTotal(response.data.total); - setSearchOffset(offset); - } catch { - setError('Label search failed'); - setTimeout(() => setError(''), 5000); - } finally { - setSearchLoading(false); - } - }; - - const handleSearchInput = (value: string) => { - setSearchQuery(value); - if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); - if (!value.trim()) { setSearchResults([]); setSearchTotal(0); return; } - searchTimeoutRef.current = setTimeout(() => searchLabels(value.trim(), 0), 300); - }; - - // ── Reprint ────────────────────────────────────────────────────────── - const reprintLabel = async (labelId: string) => { - setReprintStates(prev => ({ ...prev, [labelId]: 'downloading' })); - try { - const response = await axios.get(`${API_URL}/labels/${labelId}/reprint`, { - withCredentials: true, responseType: 'blob', - }); - triggerBlobDownload(response.data, 'application/pdf', `reprint_${labelId.slice(0, 8)}.pdf`); - } catch { - setError('Failed to generate reprint'); - setTimeout(() => setError(''), 5000); - } finally { - setReprintStates(prev => ({ ...prev, [labelId]: 'idle' })); - } - }; - - // ── Delete ─────────────────────────────────────────────────────────── - const deleteSheet = async (userSheetId: string) => { - if (!confirm('Are you sure you want to delete this sheet? This cannot be undone.')) return; - setSheetStates(prev => ({ ...prev, [userSheetId]: { ...prev[userSheetId], deleteStatus: 'deleting' } })); - try { - await axios.delete(`${API_URL}/delete-sheet/${userSheetId}`, { withCredentials: true }); - setUserSheets(userSheets.filter(s => s.id !== userSheetId)); - setSheetStates(prev => { const n = { ...prev }; delete n[userSheetId]; return n; }); - if (expandedSheetId === userSheetId) setExpandedSheetId(null); - setSuccess('Sheet deleted successfully'); - setTimeout(() => setSuccess(''), 3000); - } catch { - setError('Failed to delete sheet'); - setTimeout(() => setError(''), 5000); - setSheetStates(prev => ({ ...prev, [userSheetId]: { ...prev[userSheetId], deleteStatus: 'idle' } })); - } - }; - const handleFileChange = (e: ChangeEvent) => { const selectedFile = e.target.files?.[0] || null; setFile(selectedFile); @@ -353,7 +111,6 @@ function LabelUploader() { setProcessingStatus('Processing complete!'); setUploadProgress(100); setSuccess(response.data.message); - await fetchUserSheets(); console.log(`Total time: ${((Date.now() - startTime) / 1000).toFixed(2)}s`); setFile(null); setPreviewData(null); if (fileInputRef.current) fileInputRef.current.value = ''; @@ -408,12 +165,6 @@ function LabelUploader() { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; - const formatDate = (d: string) => new Date(d).toLocaleString(); - - const filteredSheets = filterFilename - ? userSheets.filter(s => s.original_filename.toLowerCase().includes(filterFilename.toLowerCase())) - : userSheets; - return (
@@ -573,246 +324,6 @@ function LabelUploader() {
- {/* Sheet History */} -
-
-
-

Sheet History

-

- {fetchingSheets ? 'Loading...' : filterFilename ? `${filteredSheets.length} of ${userSheets.length} sheet${userSheets.length !== 1 ? 's' : ''}` : `${userSheets.length} generated sheet${userSheets.length !== 1 ? 's' : ''}`} -

-
-
-
- - setFilterFilename(e.target.value)} - className="h-9 w-full pl-8 pr-3 rounded-[10px] border border-border bg-card text-foreground text-xs focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary placeholder:text-muted-foreground/50" - /> -
- -
-
- - {fetchingSheets ? ( -
-
-

Loading your sheets...

-
- ) : filteredSheets.length > 0 ? ( -
10 ? 'max-h-[600px] overflow-y-auto' : ''}> - - - - - - - - - - - - - {filteredSheets.map((sheet) => { - const downloadStatus = sheetStates[sheet.id]?.downloadStatus || 'idle'; - const deleteStatus = sheetStates[sheet.id]?.deleteStatus || 'idle'; - const isExpanded = expandedSheetId === sheet.id; - const numSelected = selectedSheets[sheet.id]?.size ?? 0; - - return ( - <> - - {/* Expand chevron */} - - - - - - - - - - {/* Expanded individual sheets */} - {isExpanded && ( - - - - )} - - ); - })} - -
- FilenameLabelsSheetsSizeCreatedActions
- - -
- - {sheet.original_filename} -
-
{sheet.label_count}{sheet.sheet_count}{formatFileSize(sheet.total_size_bytes)}{formatDate(sheet.created_at)} -
- - -
-
-
-
- {Array.from({ length: sheet.sheet_count }, (_, i) => i + 1).map(sheetNum => { - const key = `${sheet.id}_${sheetNum}`; - const isDownloading = singleSheetDownloadStates[key] === 'downloading'; - const isSelected = selectedSheets[sheet.id]?.has(sheetNum) ?? false; - const range = getLabelRange(sheet, sheetNum); - return ( -
- toggleSheetSelection(sheet.id, sheetNum)} - className="w-3.5 h-3.5 rounded border-border accent-primary cursor-pointer" - /> - - Sheet {sheetNum} - {range && {range}} - - -
- ); - })} -
- - {numSelected > 0 && ( -
- {numSelected} sheet{numSelected !== 1 ? 's' : ''} selected - -
- )} -
-
-
- ) : ( -
-
- -
-

- {filterFilename ? 'No sheets match your filter.' : 'No sheets generated yet. Upload a file to get started.'} -

-
- )} -
- - {/* Label Search */} -
-
-

Label Search

-

Find and reprint any label by barcode value

-
-
-
- - handleSearchInput(e.target.value)} - className="h-10 w-full pl-10 pr-4 rounded-[10px] border border-border bg-card text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary placeholder:text-muted-foreground/50" - /> -
- - {searchLoading && ( -
-
- Searching... -
- )} - - {!searchLoading && searchQuery && searchResults.length === 0 && ( -

No labels found for "{searchQuery}"

- )} - - {searchResults.length > 0 && ( -
- {searchResults.map(item => ( -
-
-

{item.barcode_value}

-

- {item.text_fields.map(tf => `${tf.label}: ${tf.value}`).join(' · ')} - {item.text_fields.length > 0 && ' · '} - {item.original_filename} · Sheet {item.sheet_number} -

-
- -
- ))} - - {searchResults.length < searchTotal && ( -
- -
- )} - -

- Showing {searchResults.length} of {searchTotal} result{searchTotal !== 1 ? 's' : ''} -

-
- )} -
-
-
{/* Preview Panel */}