- main.py: global get_current_user dependency on all routers except /auth/login and /health; prod fail-fast startup guard rejects wildcard CORS, default credentials, DEV_LOGIN_ENABLED - inventory/pm: X-User-Role header identity fallback now only active when PROCUREMENT_DEV_LOGIN_ENABLED=true (was unconditional in production) - item_decisions writes + attachment deletes: require_role( procurement_manager, admin) — require_role was defined but never used - config.py: ENVIRONMENT flag, comma-separated CORS_ORIGINS, no default Casdoor client secret/admin password, DEV_LOGIN_ENABLED=false - frontend: authenticated file downloads (Bearer + blob) — <a href> links cannot send the token; FileList/QuoteDetail/AcceptModal/EditModal/ PurchaseHistory converted; DEV_LOGIN_ENABLED build arg defaults false - smoke tests authenticate via POST /auth/login (SMOKE_USER/SMOKE_PASSWORD) - traefik: security headers, API rate limit, 12MB body cap (prod overlay) - nginx client_max_body_size aligned to 12MB
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
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;
|
||
// دانلود احراز هویتشده (توکن Bearer + blob — لینک خام توکن نمیفرستد)
|
||
onDownload?: (a: Attachment) => void | Promise<void>;
|
||
label?: string;
|
||
}
|
||
|
||
// مدیریت چند فاکتور: آپلود مستقیم به سرور (MinIO) + دانلود/حذف فایلهای ذخیرهشده
|
||
export function FileList({ files, onChange, upload, onDelete, onDownload, 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>
|
||
{onDownload && (
|
||
<button
|
||
type="button"
|
||
className="inv-open"
|
||
title="دانلود"
|
||
onClick={() => {
|
||
Promise.resolve(onDownload(inv)).catch((err) =>
|
||
toast.error(err instanceof Error ? err.message : "دانلود فایل ناموفق بود"),
|
||
);
|
||
}}
|
||
>
|
||
↓
|
||
</button>
|
||
)}
|
||
<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>
|
||
);
|
||
}
|