Skip to content

Commit 3354efb

Browse files
authored
Merge pull request #293 from ury-erp/dashboard-v3
Dashboard v3
2 parents 4439775 + fb08bdc commit 3354efb

22 files changed

Lines changed: 2556 additions & 1354 deletions

frontend/src/components/common/Drawer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@ export const Drawer: React.FC<DrawerProps> = ({
5050
if (!isOpen) return null;
5151

5252
return (
53-
<div className="fixed inset-0 z-50 overflow-hidden">
53+
<div className="fixed inset-0 z-50 overflow-hidden" onClick={(e) => e.stopPropagation()}>
5454
{/* Backdrop */}
5555
<div
5656
className="fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-300 ease-in-out"
57-
onClick={onClose}
57+
onClick={(e) => {
58+
e.stopPropagation();
59+
e.preventDefault();
60+
onClose();
61+
}}
5862
aria-hidden="true"
5963
/>
6064

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { useState, useRef, DragEvent, ChangeEvent } from 'react';
2+
import { Button } from '@ury/ui';
3+
import { Upload, FileText, X, Download } from 'lucide-react';
4+
import { parseMenuCsv, ParsedMenuRow } from '../../utils/csvParser';
5+
6+
interface MenuBulkUploadProps {
7+
onItemsParsed: (items: ParsedMenuRow[]) => void;
8+
title?: string;
9+
subtitle?: string;
10+
file?: File | null;
11+
onFileChange?: (file: File | null) => void;
12+
}
13+
14+
export function MenuBulkUpload({
15+
onItemsParsed,
16+
title = "Bulk Upload (Optional)",
17+
subtitle = "Import items from a CSV file",
18+
file = null,
19+
onFileChange,
20+
}: MenuBulkUploadProps) {
21+
const [isDragging, setIsDragging] = useState(false);
22+
const [importMessage, setImportMessage] = useState<string | null>(null);
23+
const [importError, setImportError] = useState<string | null>(null);
24+
const [localFile, setLocalFile] = useState<File | null>(null);
25+
26+
const currentFile = onFileChange ? file : localFile;
27+
const setFile = (f: File | null) => {
28+
if (onFileChange) {
29+
onFileChange(f);
30+
} else {
31+
setLocalFile(f);
32+
}
33+
};
34+
35+
const fileInputRef = useRef<HTMLInputElement>(null);
36+
const messageTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
37+
38+
const importFile = async (selectedFile: File) => {
39+
if (messageTimeoutRef.current) clearTimeout(messageTimeoutRef.current);
40+
setImportMessage(null);
41+
setImportError(null);
42+
setFile(selectedFile);
43+
44+
try {
45+
const text = await selectedFile.text();
46+
const rows = parseMenuCsv(text);
47+
48+
if (rows.length === 0) {
49+
setImportError("Couldn't read that file, make sure it matches the template format.");
50+
setFile(null);
51+
return;
52+
}
53+
54+
onItemsParsed(rows);
55+
setImportMessage(`Imported ${rows.length} item${rows.length === 1 ? '' : 's'} from ${selectedFile.name}, review and edit below.`);
56+
messageTimeoutRef.current = setTimeout(() => setImportMessage(null), 5000);
57+
} catch {
58+
setImportError("Couldn't read that file, make sure it matches the template format.");
59+
setFile(null);
60+
}
61+
};
62+
63+
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
64+
e.preventDefault();
65+
setIsDragging(true);
66+
};
67+
68+
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
69+
e.preventDefault();
70+
setIsDragging(false);
71+
};
72+
73+
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
74+
e.preventDefault();
75+
setIsDragging(false);
76+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
77+
importFile(e.dataTransfer.files[0]);
78+
}
79+
};
80+
81+
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
82+
if (e.target.files && e.target.files[0]) {
83+
importFile(e.target.files[0]);
84+
}
85+
};
86+
87+
return (
88+
<div className="space-y-3">
89+
<div className="flex items-center justify-between">
90+
<div>
91+
<h3 className="text-sm font-semibold text-gray-900">{title}</h3>
92+
{subtitle && <p className="text-xs text-gray-500 mt-0.5">{subtitle}</p>}
93+
</div>
94+
<a
95+
href="/assets/ury/files/menu_template.csv"
96+
download
97+
className="text-xs font-semibold text-primary hover:underline flex items-center gap-1"
98+
>
99+
<Download className="w-3.5 h-3.5" /> Download Template
100+
</a>
101+
</div>
102+
103+
<input
104+
ref={fileInputRef}
105+
type="file"
106+
accept=".csv"
107+
onChange={handleFileSelect}
108+
className="hidden"
109+
/>
110+
111+
{currentFile ? (
112+
<div className="p-3 border border-primary bg-primary/10 rounded-lg flex items-center justify-between">
113+
<div className="flex items-center gap-2">
114+
<FileText className="w-4 h-4 text-primary" />
115+
<div>
116+
<p className="text-xs font-medium text-foreground">{currentFile.name}</p>
117+
<p className="text-[10px] text-muted-foreground">{(currentFile.size / 1024).toFixed(1)} KB</p>
118+
</div>
119+
</div>
120+
<Button
121+
type="button"
122+
variant="ghost"
123+
onClick={() => {
124+
setFile(null);
125+
setImportMessage(null);
126+
setImportError(null);
127+
}}
128+
className="text-muted-foreground hover:text-red-600 p-1 h-auto"
129+
title="Remove File"
130+
>
131+
<X className="w-4 h-4" />
132+
</Button>
133+
</div>
134+
) : (
135+
<div
136+
onDragOver={handleDragOver}
137+
onDragLeave={handleDragLeave}
138+
onDrop={handleDrop}
139+
onClick={() => fileInputRef.current?.click()}
140+
className={`p-4 border border-dashed rounded-lg text-center cursor-pointer transition-colors ${
141+
isDragging
142+
? 'border-primary bg-primary/10'
143+
: 'border-gray-200 hover:border-primary bg-gray-50'
144+
}`}
145+
>
146+
<Upload className="w-5 h-5 text-gray-400 mx-auto mb-1.5" />
147+
<p className="text-xs font-medium text-gray-700">
148+
Drag &amp; drop CSV file here, or <span className="text-primary hover:underline font-semibold">browse</span>
149+
</p>
150+
<p className="text-[10px] text-gray-500 mt-0.5">Supports CSV files only</p>
151+
</div>
152+
)}
153+
154+
{importMessage && (
155+
<p className="text-xs font-medium text-primary">{importMessage}</p>
156+
)}
157+
{importError && (
158+
<p className="text-xs font-medium text-red-600">{importError}</p>
159+
)}
160+
</div>
161+
);
162+
}

0 commit comments

Comments
 (0)