Ver código fonte

Truck Routing Summary List

MergeProblem1
B.E.N.S.O.N 16 horas atrás
pai
commit
9737d94e49
3 arquivos alterados com 183 adições e 0 exclusões
  1. +52
    -0
      src/app/(main)/report/truckRoutingSummaryApi.ts
  2. +3
    -0
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  3. +128
    -0
      src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx

+ 52
- 0
src/app/(main)/report/truckRoutingSummaryApi.ts Ver arquivo

@@ -0,0 +1,52 @@
"use client";

import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api";

export interface ReportOption {
label: string;
value: string;
}

export async function fetchTruckRoutingStoreOptions(): Promise<ReportOption[]> {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/truck-routing-summary/store-options`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
}
);

if (!response.ok) {
throw new Error(`Failed to fetch store options: ${response.status}`);
}

const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map((item: any) => ({
label: item?.label ?? item?.value ?? "",
value: item?.value ?? "",
}));
}

export async function fetchTruckRoutingLaneOptions(storeId?: string): Promise<ReportOption[]> {
const qs = storeId ? `?storeId=${encodeURIComponent(storeId)}` : "";
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/truck-routing-summary/lane-options${qs}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
}
);

if (!response.ok) {
throw new Error(`Failed to fetch lane options: ${response.status}`);
}

const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map((item: any) => ({
label: item?.label ?? item?.value ?? "",
value: item?.value ?? "",
}));
}

+ 3
- 0
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Ver arquivo

@@ -41,6 +41,7 @@ import dayjs, { Dayjs } from 'dayjs';
import { PrinterCombo } from "@/app/api/settings/printer";
import { Autocomplete } from "@mui/material";
import FGPickOrderTicketReleaseTable from "./FGPickOrderTicketReleaseTable";
import TruckRoutingSummaryTab from "./TruckRoutingSummaryTab";

interface Props {
pickOrders: PickOrderResult[];
@@ -710,6 +711,7 @@ const handleAssignByLane = useCallback(async (
<Tab label={t("Finished Good Record")} iconPosition="end" />
<Tab label={t("Ticket Release Table")} iconPosition="end" />
<Tab label={t("Finished Good Record (All)")} iconPosition="end" />
<Tab label="送貨路線摘要" iconPosition="end" />
</Tabs>
</Box>
@@ -764,6 +766,7 @@ const handleAssignByLane = useCallback(async (
listScope="all"
/>
)}
{tabIndex === 5 && <TruckRoutingSummaryTab />}
</Box>
</Box>
);


+ 128
- 0
src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx Ver arquivo

@@ -0,0 +1,128 @@
"use client";

import { useEffect, useState } from "react";
import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { fetchTruckRoutingLaneOptions, fetchTruckRoutingStoreOptions, ReportOption } from "@/app/(main)/report/truckRoutingSummaryApi";

const TruckRoutingSummaryTab: React.FC = () => {
const [storeOptions, setStoreOptions] = useState<ReportOption[]>([]);
const [laneOptions, setLaneOptions] = useState<ReportOption[]>([]);
const [storeId, setStoreId] = useState("");
const [truckLanceCode, setTruckLanceCode] = useState("");
const [date, setDate] = useState("");
const [loading, setLoading] = useState(false);

useEffect(() => {
fetchTruckRoutingStoreOptions()
.then(setStoreOptions)
.catch((err) => console.error("Failed to load store options", err));
}, []);

const onStoreChange = async (value: string) => {
setStoreId(value);
setTruckLanceCode("");
try {
const lanes = await fetchTruckRoutingLaneOptions(value);
setLaneOptions(lanes);
} catch (error) {
console.error("Failed to load lane options", error);
setLaneOptions([]);
}
};

const canDownload = storeId && truckLanceCode && date && !loading;

const onDownload = async () => {
if (!canDownload) return;
setLoading(true);
try {
const qs = new URLSearchParams({
storeId,
truckLanceCode,
date,
}).toString();
const url = `${NEXT_PUBLIC_API_URL}/truck-routing-summary/print?${qs}`;
const response = await clientAuthFetch(url, {
method: "GET",
headers: { Accept: "application/pdf" },
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}

const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.setAttribute("download", "TruckRoutingSummary.pdf");
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error("Failed to download Truck Routing Summary", error);
alert("下載送貨路線摘要失敗,請稍後再試。");
} finally {
setLoading(false);
}
};

return (
<Box sx={{ maxWidth: 820 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
送貨路線摘要
</Typography>
<Stack direction={{ xs: "column", md: "row" }} spacing={2} sx={{ mb: 2 }}>
<TextField
select
fullWidth
label="2/F 或 4/F"
value={storeId}
onChange={(e) => onStoreChange(e.target.value)}
>
{storeOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</TextField>
<TextField
select
fullWidth
label="車線"
value={truckLanceCode}
onChange={(e) => setTruckLanceCode(e.target.value)}
disabled={!storeId}
>
{laneOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="日期"
type="date"
value={date}
InputLabelProps={{ shrink: true }}
onChange={(e) => setDate(e.target.value)}
/>
</Stack>
<Button
variant="contained"
startIcon={<DownloadIcon />}
disabled={!canDownload}
onClick={onDownload}
>
{loading ? "生成中..." : "下載報告 (PDF)"}
</Button>
</Box>
);
};

export default TruckRoutingSummaryTab;

Carregando…
Cancelar
Salvar