diff --git a/src/app/api/bag/action.ts b/src/app/api/bag/action.ts index 8d4bc21..7668b70 100644 --- a/src/app/api/bag/action.ts +++ b/src/app/api/bag/action.ts @@ -118,4 +118,27 @@ export const fetchBagLotLines = cache(async (bagId: number) => export const fetchBagConsumptions = cache(async (bagLotLineId: number) => serverFetchJson(`${BASE_API_URL}/bag/lot-lines/${bagLotLineId}/consumptions`, { method: "GET" }) -); \ No newline at end of file +); + +export interface SoftDeleteBagResponse { + id: number | null; + code: string | null; + name: string | null; + type: string | null; + message: string | null; + errorPosition: string | null; + entity: any | null; +} + +export const softDeleteBagByItemId = async (itemId: number): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/bag/by-item/${itemId}/soft-delete`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + } + ); + revalidateTag("bagInfo"); + revalidateTag("bags"); + return response; +}; \ No newline at end of file diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index ff20f0a..497122b 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -197,9 +197,12 @@ export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: ); }); -export const fetchTruckScheduleDashboard = cache(async () => { +export const fetchTruckScheduleDashboard = cache(async (date?: string) => { + const url = date + ? `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard?date=${date}` + : `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`; return await serverFetchJson( - `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`, + url, { method: "GET", } diff --git a/src/app/api/do/client.ts b/src/app/api/do/client.ts index 8adddde..253ba12 100644 --- a/src/app/api/do/client.ts +++ b/src/app/api/do/client.ts @@ -5,8 +5,8 @@ import { type TruckScheduleDashboardItem } from "./actions"; -export const fetchTruckScheduleDashboardClient = async (): Promise => { - return await fetchTruckScheduleDashboard(); +export const fetchTruckScheduleDashboardClient = async (date?: string): Promise => { + return await fetchTruckScheduleDashboard(date); }; export type { TruckScheduleDashboardItem }; diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index 1340c6e..3f4b782 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -45,6 +45,7 @@ export type CreateItemInputs = { isEgg?: boolean | undefined; isFee?: boolean | undefined; isBag?: boolean | undefined; + qcType?: string | undefined; }; export const saveItem = async (data: CreateItemInputs) => { diff --git a/src/app/api/settings/qcCategory/client.ts b/src/app/api/settings/qcCategory/client.ts new file mode 100644 index 0000000..e77e7f5 --- /dev/null +++ b/src/app/api/settings/qcCategory/client.ts @@ -0,0 +1,28 @@ +"use client"; + +import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { QcItemInfo } from "./index"; + +export const fetchQcItemsByCategoryId = async (categoryId: number): Promise => { + const token = localStorage.getItem("accessToken"); + + const response = await fetch(`${NEXT_PUBLIC_API_URL}/qcCategories/${categoryId}/items`, { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + if (response.status === 401) { + throw new Error("Unauthorized: Please log in again"); + } + throw new Error(`Failed to fetch QC items: ${response.status} ${response.statusText}`); + } + + return response.json(); +}; + + + diff --git a/src/app/api/settings/qcCategory/index.ts b/src/app/api/settings/qcCategory/index.ts index 9e38697..974047a 100644 --- a/src/app/api/settings/qcCategory/index.ts +++ b/src/app/api/settings/qcCategory/index.ts @@ -17,6 +17,15 @@ export interface QcCategoryCombo { label: string; } +export interface QcItemInfo { + id: number; + qcItemId: number; + code: string; + name?: string; + order: number; + description?: string; +} + export const preloadQcCategory = () => { fetchQcCategories(); }; diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index 661cf6e..583c110 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -31,6 +31,7 @@ import { saveItemQcChecks } from "@/app/api/settings/qcCheck/actions"; import { useGridApiRef } from "@mui/x-data-grid"; import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; import { WarehouseResult } from "@/app/api/warehouse"; +import { softDeleteBagByItemId } from "@/app/api/bag/action"; type Props = { isEditMode: boolean; @@ -173,6 +174,16 @@ const CreateItem: React.FC = ({ ); } else if (!Boolean(responseQ.id)) { } else if (Boolean(responseI.id) && Boolean(responseQ.id)) { + // If special type is not "isBag", soft-delete the bag record if it exists + if (data.isBag !== true && data.id) { + try { + const itemId = typeof data.id === "string" ? parseInt(data.id) : data.id; + await softDeleteBagByItemId(itemId); + } catch (bagError) { + // Log error but don't block the save operation + console.log("Error soft-deleting bag:", bagError); + } + } router.replace(redirPath); } } diff --git a/src/components/CreateItem/ProductDetails.tsx b/src/components/CreateItem/ProductDetails.tsx index 0903a54..2be33c1 100644 --- a/src/components/CreateItem/ProductDetails.tsx +++ b/src/components/CreateItem/ProductDetails.tsx @@ -29,8 +29,10 @@ import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; import { TypeEnum } from "@/app/utils/typeEnum"; import { CreateItemInputs } from "@/app/api/settings/item/actions"; import { ItemQc } from "@/app/api/settings/item"; -import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; +import { QcCategoryCombo, QcItemInfo } from "@/app/api/settings/qcCategory"; +import { fetchQcItemsByCategoryId } from "@/app/api/settings/qcCategory/client"; import { WarehouseResult } from "@/app/api/warehouse"; +import QcItemsList from "./QcItemsList"; type Props = { // isEditMode: boolean; // type: TypeEnum; @@ -43,11 +45,13 @@ type Props = { }; const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => { + const [qcItems, setQcItems] = useState([]); + const [qcItemsLoading, setQcItemsLoading] = useState(false); const { t, i18n: { language }, - } = useTranslation(); + } = useTranslation("items"); const { register, @@ -121,6 +125,30 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehous } }, [initialDefaultValues, setValue, getValues]); + // Watch qcCategoryId and fetch QC items when it changes + const qcCategoryId = watch("qcCategoryId"); + + useEffect(() => { + const fetchItems = async () => { + if (qcCategoryId) { + setQcItemsLoading(true); + try { + const items = await fetchQcItemsByCategoryId(qcCategoryId); + setQcItems(items); + } catch (error) { + console.error("Failed to fetch QC items:", error); + setQcItems([]); + } finally { + setQcItemsLoading(false); + } + } else { + setQcItems([]); + } + }; + + fetchItems(); + }, [qcCategoryId]); + return ( @@ -216,6 +244,26 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehous )} /> + + ( + + {t("QC Type")} + + + )} + /> + = ({ isEditMode, qcCategoryCombo, warehous + + + = ({ + qcItems, + loading = false, + categorySelected = false, +}) => { + const { t } = useTranslation("items"); + + // Sort items by order + const sortedItems = [...qcItems].sort((a, b) => a.order - b.order); + + if (loading) { + return ( + + + + {t("Loading QC items...")} + + + ); + } + + if (!categorySelected) { + return ( + + + + {t("Select a QC template to view items")} + + + ); + } + + if (sortedItems.length === 0) { + return ( + + + + {t("No QC items in this template")} + + + ); + } + + return ( + + + + + + {t("QC Checklist")} ({sortedItems.length}) + + + + + {sortedItems.map((item, index) => ( + + {index > 0 && } + + + {/* Order Number */} + + {item.order}. + + + {/* Content */} + + + {item.name || item.code} + + {item.description && ( + + {item.description} + + )} + + + + + ))} + + + ); +}; + +export default QcItemsList; + diff --git a/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx b/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx index 00d3d7c..94476d9 100644 --- a/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx +++ b/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx @@ -35,6 +35,7 @@ interface CompletedTracker { const TruckScheduleDashboard: React.FC = () => { const { t } = useTranslation("dashboard"); const [selectedStore, setSelectedStore] = useState(""); + const [selectedDate, setSelectedDate] = useState("today"); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); // Initialize as null to avoid SSR/client hydration mismatch @@ -43,6 +44,23 @@ const TruckScheduleDashboard: React.FC = () => { const completedTrackerRef = useRef>(new Map()); const refreshCountRef = useRef(0); + // Get date label for display (e.g., "2026-01-17") + const getDateLabel = (offset: number): string => { + return dayjs().add(offset, 'day').format('YYYY-MM-DD'); + }; + + // Convert date option to YYYY-MM-DD format for API + const getDateParam = (dateOption: string): string => { + if (dateOption === "today") { + return dayjs().format('YYYY-MM-DD'); + } else if (dateOption === "tomorrow") { + return dayjs().add(1, 'day').format('YYYY-MM-DD'); + } else if (dateOption === "dayAfterTomorrow") { + return dayjs().add(2, 'day').format('YYYY-MM-DD'); + } + return dayjs().add(1, 'day').format('YYYY-MM-DD'); + }; + // Set client flag and time on mount useEffect(() => { setIsClient(true); @@ -136,7 +154,8 @@ const TruckScheduleDashboard: React.FC = () => { // Load data from API const loadData = useCallback(async () => { try { - const result = await fetchTruckScheduleDashboardClient(); + const dateParam = getDateParam(selectedDate); + const result = await fetchTruckScheduleDashboardClient(dateParam); // Update completed tracker refreshCountRef.current += 1; @@ -175,7 +194,7 @@ const TruckScheduleDashboard: React.FC = () => { } finally { setLoading(false); } - }, []); + }, [selectedDate]); // Initial load and auto-refresh every 5 minutes useEffect(() => { @@ -183,7 +202,7 @@ const TruckScheduleDashboard: React.FC = () => { const refreshInterval = setInterval(() => { loadData(); - }, 5 * 60 * 1000); // 5 minutes + }, 0.1 * 60 * 1000); // 5 minutes return () => clearInterval(refreshInterval); }, [loadData]); @@ -256,6 +275,23 @@ const TruckScheduleDashboard: React.FC = () => { 4/F + + + + {t("Select Date")} + + + {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} @@ -290,7 +326,7 @@ const TruckScheduleDashboard: React.FC = () => { - {t("No truck schedules available for today")} + {t("No truck schedules available")} ({getDateParam(selectedDate)}) diff --git a/src/i18n/en/dashboard.json b/src/i18n/en/dashboard.json index 7d5f025..5a1c9aa 100644 --- a/src/i18n/en/dashboard.json +++ b/src/i18n/en/dashboard.json @@ -73,6 +73,11 @@ "Last Ticket End": "Last Ticket End", "Pick Time (min)": "Pick Time (min)", "No truck schedules available for today": "No truck schedules available for today", + "No truck schedules available": "No truck schedules available", + "Select Date": "Select Date", + "Today": "Today", + "Tomorrow": "Tomorrow", + "Day After Tomorrow": "Day After Tomorrow", "Goods Receipt Status": "Goods Receipt Status", "Filter": "Filter", "All": "All", diff --git a/src/i18n/en/items.json b/src/i18n/en/items.json index 40d8912..cf58fad 100644 --- a/src/i18n/en/items.json +++ b/src/i18n/en/items.json @@ -9,5 +9,12 @@ "Back": "Back", "Status": "Status", "Complete": "Complete", - "Missing Data": "Missing Data" + "Missing Data": "Missing Data", + "Loading QC items...": "Loading QC items...", + "Select a QC template to view items": "Select a QC template to view items", + "No QC items in this template": "No QC items in this template", + "QC Checklist": "QC Checklist", + "QC Type": "QC Type", + "IPQC": "IPQC", + "EPQC": "EPQC" } \ No newline at end of file diff --git a/src/i18n/zh/dashboard.json b/src/i18n/zh/dashboard.json index 6fdfaec..0eb797a 100644 --- a/src/i18n/zh/dashboard.json +++ b/src/i18n/zh/dashboard.json @@ -73,6 +73,11 @@ "Last Ticket End": "末單結束時間", "Pick Time (min)": "揀貨時間(分鐘)", "No truck schedules available for today": "今日無車輛調度計劃", + "No truck schedules available": "無車輛調度計劃", + "Select Date": "請選擇日期", + "Today": "是日", + "Tomorrow": "翌日", + "Day After Tomorrow": "後日", "Goods Receipt Status": "貨物接收狀態", "Filter": "篩選", "All": "全部", diff --git a/src/i18n/zh/items.json b/src/i18n/zh/items.json index c2f200a..613ac3e 100644 --- a/src/i18n/zh/items.json +++ b/src/i18n/zh/items.json @@ -43,5 +43,12 @@ "Back": "返回", "Status": "狀態", "Complete": "完成", -"Missing Data": "缺少資料" +"Missing Data": "缺少資料", +"Loading QC items...": "正在加載質檢項目...", +"Select a QC template to view items": "選擇質檢模板以查看項目", +"No QC items in this template": "此模板無質檢項目", +"QC Checklist": "質檢項目", +"QC Type": "質檢種類", +"IPQC": "IPQC", +"EPQC": "EPQC" } \ No newline at end of file