feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { Attachment } from "../lib/types";
|
||||
|
||||
export interface FileListProps {
|
||||
files: Attachment[];
|
||||
onChange: (files: Attachment[]) => void;
|
||||
// آپلود واقعی هر فایل به سرور (باید Attachment با id برگرداند)
|
||||
upload: (file: File) => Promise<Attachment>;
|
||||
// حذف سمت سرور (اختیاری — بعد از حذف محلی صدا زده میشود)
|
||||
onDelete?: (a: Attachment) => void;
|
||||
// لینک دانلود؛ null یعنی هنوز آپلود نشده
|
||||
urlOf?: (a: Attachment) => string | null;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
// مدیریت چند فاکتور: آپلود مستقیم به سرور (MinIO) + دانلود/حذف فایلهای ذخیرهشده
|
||||
export function FileList({ files, onChange, upload, onDelete, urlOf, label = "افزودن فاکتور" }: FileListProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const remove = (i: number) => {
|
||||
const removed = files[i];
|
||||
onChange(files.filter((_, idx) => idx !== i));
|
||||
if (removed) onDelete?.(removed);
|
||||
};
|
||||
|
||||
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const picked = Array.from(e.target.files || []);
|
||||
e.target.value = ""; // اجازهی انتخاب دوبارهی همان فایل
|
||||
if (!picked.length) return;
|
||||
const added: Attachment[] = [];
|
||||
for (const file of picked) {
|
||||
try {
|
||||
added.push(await upload(file));
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : `آپلود «${file.name}» ناموفق بود`);
|
||||
}
|
||||
}
|
||||
if (added.length) onChange([...files, ...added]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!files.length && <div className="inv-empty">هنوز فایلی انتخاب نشده است.</div>}
|
||||
{files.map((inv, i) => (
|
||||
<div key={i} className="inv-row">
|
||||
<span className="inv-name">📎 {inv.name}</span>
|
||||
{urlOf?.(inv) && (
|
||||
<a className="inv-open" href={urlOf(inv)!} download={inv.name} title="دانلود">
|
||||
↓
|
||||
</a>
|
||||
)}
|
||||
<button type="button" className="inv-x" title="حذف" onClick={() => remove(i)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*,.pdf,.png,.jpg,.jpeg"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<button type="button" className="btn-line-file" onClick={() => inputRef.current?.click()}>
|
||||
+ {label}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user