fix(inventory): envelope transfer responses and add defensive array guards in UI

This commit is contained in:
2026-09-23 22:46:07 +03:30
parent 856513b6d7
commit e560018273
4 changed files with 69 additions and 37 deletions
+36 -11
View File
@@ -243,20 +243,32 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
}
async function getData<T>(path: string): Promise<T> {
const res = await request<{ data: T }>(path);
return res.data;
const res = await request<any>(path);
if (res && typeof res === "object" && "data" in res) {
return res.data as T;
}
return res as T;
}
async function postData<T>(path: string, body: unknown): Promise<T> {
const res = await request<{ data: T }>(path, { method: "POST", body: JSON.stringify(body) });
return res.data;
const res = await request<any>(path, { method: "POST", body: JSON.stringify(body) });
if (res && typeof res === "object" && "data" in res) {
return res.data as T;
}
return res as T;
}
async function patchData<T>(path: string, body: unknown): Promise<T> {
const res = await request<{ data: T }>(path, { method: "PATCH", body: JSON.stringify(body) });
return res.data;
const res = await request<any>(path, { method: "PATCH", body: JSON.stringify(body) });
if (res && typeof res === "object" && "data" in res) {
return res.data as T;
}
return res as T;
}
async function deleteData<T>(path: string): Promise<T> {
const res = await request<{ data: T }>(path, { method: "DELETE" });
return res.data;
const res = await request<any>(path, { method: "DELETE" });
if (res && typeof res === "object" && "data" in res) {
return res.data as T;
}
return res as T;
}
// آپلود چندبخشی (multipart) — بدون هدر Content-Type دستی تا مرورگر
@@ -1758,7 +1770,13 @@ export const apiV26 = {
if (direction) q.set("direction", direction);
if (status) q.set("status", status);
const qs = q.toString() ? `?${q.toString()}` : "";
return getData<ProjectTransfer[]>(`/inventory/project-transfers${qs}`);
try {
const res = await getData<ProjectTransfer[]>(`/inventory/project-transfers${qs}`);
return Array.isArray(res) ? res : [];
} catch (err) {
console.error("Failed to fetch project transfers:", err);
return [];
}
},
get: async (id: string, projectId?: string | null): Promise<ProjectTransfer> =>
getData<ProjectTransfer>(projectId ? `/inventory/project-transfers/${id}?project_id=${projectId}` : `/inventory/project-transfers/${id}`),
@@ -1768,8 +1786,15 @@ export const apiV26 = {
patchData<ProjectTransfer>(projectId ? `/inventory/project-transfers/${id}?project_id=${projectId}` : `/inventory/project-transfers/${id}`, patch),
delete: async (id: string, projectId?: string | null): Promise<{ ok: boolean; message: string }> =>
deleteData(projectId ? `/inventory/project-transfers/${id}?project_id=${projectId}` : `/inventory/project-transfers/${id}`),
onTheWay: async (projectId?: string | null): Promise<ProjectTransfer[]> =>
getData<ProjectTransfer[]>(projectId ? `/pm/transfers/on-the-way?project_id=${projectId}` : `/pm/transfers/on-the-way`),
onTheWay: async (projectId?: string | null): Promise<ProjectTransfer[]> => {
try {
const res = await getData<ProjectTransfer[]>(projectId ? `/pm/transfers/on-the-way?project_id=${projectId}` : `/pm/transfers/on-the-way`);
return Array.isArray(res) ? res : [];
} catch (err) {
console.error("Failed to fetch on-the-way transfers:", err);
return [];
}
},
receive: async (id: string, body: ProjectTransferReceiveIn, projectId?: string | null): Promise<ProjectTransfer> =>
postData<ProjectTransfer>(projectId ? `/inventory/project-transfers/${id}/receive?project_id=${projectId}` : `/inventory/project-transfers/${id}/receive`, body),
},
@@ -43,11 +43,13 @@ export function ArrivalsPage() {
apiV26.warehouses.list(activeProject),
apiV26.projectTransfers.onTheWay(activeProject),
]);
setItems(arr);
setWarehouses(whs);
setIncomingTransfers(incoming);
setItems(Array.isArray(arr) ? arr : []);
setWarehouses(Array.isArray(whs) ? whs : []);
setIncomingTransfers(Array.isArray(incoming) ? incoming : []);
} catch (err) {
toast.error(err instanceof Error ? err.message : "خطا در دریافت کالاهای در راه");
setItems([]);
setIncomingTransfers([]);
} finally {
setLoading(false);
}
@@ -290,19 +292,19 @@ export function ArrivalsPage() {
{loading ? (
<div className="py-6 text-center text-[12.5px] text-muted">در حال بارگذاری…</div>
) : !orders.length && !incomingTransfers.length ? (
) : !(orders || []).length && !(incomingTransfers || []).length ? (
<EmptyState emoji="🚚" text="کالای در راهی برای دریافت وجود ندارد." />
) : (
<>
{/* بخش انتقال‌های بین کارگاهی در راه */}
{incomingTransfers.length > 0 && (
{(incomingTransfers || []).length > 0 && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h3 className="text-[14px] font-bold text-foreground">🔁 انتقال‌های بین کارگاهی در راه ({faNum(incomingTransfers.length)})</h3>
<h3 className="text-[14px] font-bold text-foreground">🔁 انتقال‌های بین کارگاهی در راه ({faNum((incomingTransfers || []).length)})</h3>
<span className="text-[11.5px] text-muted">— تحویل ۱۰۰٪ کامل مصالح ارسالی از سایر پروژه‌ها</span>
</div>
{incomingTransfers.map((t) => {
{(incomingTransfers || []).map((t) => {
const draftAllocs = transferDraftOf(t);
const allocSum = round4(draftAllocs.reduce((s, a) => s + (parseFloat(a.qty) || 0), 0));
const targetQty = parseFloat(String(t.quantity));
@@ -216,16 +216,20 @@ export function ChangeInventoryPage() {
setLoadingTransfers(true);
try {
const list = await apiV26.projectTransfers.list(user.project_id, "outgoing");
setTransfers(list);
setTransfers(Array.isArray(list) ? list : []);
} catch (err) {
console.error("Failed to load project transfers:", err);
setTransfers([]);
} finally {
setLoadingTransfers(false);
}
}, [user?.project_id]);
useEffect(() => {
api.projects.list().then(setAllProjects).catch(() => {});
api.projects
.list()
.then((res) => setAllProjects(Array.isArray(res) ? res : []))
.catch(() => setAllProjects([]));
}, []);
useEffect(() => {
@@ -651,7 +655,7 @@ export function ChangeInventoryPage() {
onChange={(e) => setToProjectId(e.target.value)}
>
<option value="">— انتخاب پروژه مقصد —</option>
{allProjects
{(allProjects || [])
.filter((p) => p.id !== user?.project_id)
.map((p) => (
<option key={p.id} value={p.id}>
@@ -840,11 +844,11 @@ export function ChangeInventoryPage() {
onChange={(e) => setNote(e.target.value)}
/>
</div>
{type === "PROJECT_TRANSFER" && matchedStock && matchedStock.length > 0 && (
{type === "PROJECT_TRANSFER" && (matchedStock?.length ?? 0) > 0 && (
<div className="field md:col-span-2 rounded-lg border border-line/70 bg-bg/50 p-3 text-xs">
<div className="font-semibold text-muted mb-1.5">موجودی‌های فعلی این متریال در انبار مبدأ ({whName(warehouses, whId)}):</div>
<div className="flex flex-wrap gap-2">
{matchedStock.map((s) => (
{(matchedStock || []).map((s) => (
<div key={s.id} className="rounded-md border border-line bg-panel px-2.5 py-1 text-[11.5px] shadow-2xs">
<span className="font-medium">{s.brand?.name || "بدون برند"}</span>
<span className="text-muted"> — {Object.entries(s.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "بدون مشخصه"} — </span>
@@ -940,7 +944,7 @@ export function ChangeInventoryPage() {
{loadingTransfers ? (
<div className="py-8 text-center text-xs text-muted">در حال بارگذاری سوابق…</div>
) : !transfers.length ? (
) : !(transfers || []).length ? (
<div className="py-8 text-center text-xs text-muted">هیچ انتقال بین کارگاهی از این پروژه ثبت نشده است.</div>
) : (
<div className="mt-3 overflow-x-auto">
@@ -957,7 +961,7 @@ export function ChangeInventoryPage() {
</tr>
</thead>
<tbody className="divide-y divide-line/50">
{transfers.map((t) => {
{(transfers || []).map((t) => {
const isInTransit = t.status === "in_transit";
return (
<tr key={t.id} className="hover:bg-bg/40">
@@ -1057,7 +1061,7 @@ export function ChangeInventoryPage() {
value={editToProjectId}
onChange={(e) => setEditToProjectId(e.target.value)}
>
{allProjects
{(allProjects || [])
.filter((p) => p.id !== user?.project_id)
.map((p) => (
<option key={p.id} value={p.id}>