diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index fc2a73e..2c163d5 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -87,7 +87,7 @@ export default function ProductionSchedulePage() { setLoading(true); try { const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule`, { - method: 'POST', + method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { @@ -104,7 +104,7 @@ export default function ProductionSchedulePage() { const handleExport = async () => { const token = localStorage.getItem("accessToken"); try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/export-prod-schedule`, { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); diff --git a/src/app/(main)/settings/qcItem copy/create/not-found.tsx b/src/app/(main)/settings/qcItem copy/create/not-found.tsx new file mode 100644 index 0000000..b36d891 --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/create/not-found.tsx @@ -0,0 +1,19 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("qcItem", "common"); + + return ( + + {t("Not Found")} + + {t("The create qc item page was not found!")} + + + {t("Return to all qc items")} + + + ); +} diff --git a/src/app/(main)/settings/qcItem copy/create/page.tsx b/src/app/(main)/settings/qcItem copy/create/page.tsx new file mode 100644 index 0000000..1cb5c8a --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/create/page.tsx @@ -0,0 +1,26 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSave from "@/components/QcItemSave"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +const qcItem: React.FC = async () => { + const { t } = await getServerI18n("qcItem"); + + return ( + <> + + {t("Create Qc Item")} + + + + + + ); +}; + +export default qcItem; diff --git a/src/app/(main)/settings/qcItem copy/edit/not-found.tsx b/src/app/(main)/settings/qcItem copy/edit/not-found.tsx new file mode 100644 index 0000000..e9e09bc --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/edit/not-found.tsx @@ -0,0 +1,19 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("qcItem", "common"); + + return ( + + {t("Not Found")} + + {t("The edit qc item page was not found!")} + + + {t("Return to all qc items")} + + + ); +} diff --git a/src/app/(main)/settings/qcItem copy/edit/page.tsx b/src/app/(main)/settings/qcItem copy/edit/page.tsx new file mode 100644 index 0000000..0e433fb --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/edit/page.tsx @@ -0,0 +1,53 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { fetchQcItemDetails, preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSave from "@/components/QcItemSave"; +import { isArray } from "lodash"; +import { notFound } from "next/navigation"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +interface Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +const qcItem: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("qcItem"); + + const id = searchParams["id"]; + + if (!id || isArray(id)) { + notFound(); + } + + try { + console.log("first"); + await fetchQcItemDetails(id); + console.log("firsts"); + } catch (e) { + if ( + e instanceof ServerFetchError && + (e.response?.status === 404 || e.response?.status === 400) + ) { + console.log(e); + notFound(); + } + } + + return ( + <> + + {t("Edit Qc Item")} + + + + + + ); +}; + +export default qcItem; diff --git a/src/app/(main)/settings/qcItem copy/page.tsx b/src/app/(main)/settings/qcItem copy/page.tsx new file mode 100644 index 0000000..f1b4e71 --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/page.tsx @@ -0,0 +1,48 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Button, Link, Stack } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { Suspense } from "react"; +import { preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSearch from "@/components/QcItemSearch"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +const qcItem: React.FC = async () => { + const { t } = await getServerI18n("qcItem"); + + preloadQcItem(); + + return ( + <> + + + {t("Qc Item")} + + + + }> + + + + + + ); +}; + +export default qcItem; diff --git a/src/app/(main)/settings/qcItemAll/page.tsx b/src/app/(main)/settings/qcItemAll/page.tsx new file mode 100644 index 0000000..ff0d328 --- /dev/null +++ b/src/app/(main)/settings/qcItemAll/page.tsx @@ -0,0 +1,47 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Stack } from "@mui/material"; +import { Suspense } from "react"; +import QcItemAllTabs from "@/components/QcItemAll/QcItemAllTabs"; +import Tab0ItemQcCategoryMapping from "@/components/QcItemAll/Tab0ItemQcCategoryMapping"; +import Tab1QcCategoryQcItemMapping from "@/components/QcItemAll/Tab1QcCategoryQcItemMapping"; +import Tab2QcCategoryManagement from "@/components/QcItemAll/Tab2QcCategoryManagement"; +import Tab3QcItemManagement from "@/components/QcItemAll/Tab3QcItemManagement"; + +export const metadata: Metadata = { + title: "Qc Item All", +}; + +const qcItemAll: React.FC = async () => { + const { t } = await getServerI18n("qcItemAll"); + + return ( + <> + + + {t("Qc Item All")} + + + Loading...}> + + } + tab1Content={} + tab2Content={} + tab3Content={} + /> + + + + ); +}; + +export default qcItemAll; + diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index 4c64f92..3efaf70 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -4,13 +4,47 @@ import React, { useState } from "react"; import { Box, Grid, Paper, Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Stack, Table, - TableBody, TableCell, TableContainer, TableHead, TableRow + TableBody, TableCell, TableContainer, TableHead, TableRow, + Tabs, Tab // ← Added for tabs } from "@mui/material"; import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; import dayjs from "dayjs"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; +// Simple TabPanel component for conditional rendering +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + return ( + + ); +} + export default function TestingPage() { + // Tab state + const [tabValue, setTabValue] = useState(0); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + // --- 1. TSC Section States --- const [tscConfig, setTscConfig] = useState({ ip: '192.168.1.100', port: '9100' }); const [tscItems, setTscItems] = useState([ @@ -35,10 +69,22 @@ export default function TestingPage() { }); // --- 4. Laser Section States --- -const [laserConfig, setLaserConfig] = useState({ ip: '192.168.1.102', port: '8080' }); -const [laserItems, setLaserItems] = useState([ - { id: 1, templateId: 'JOB_001', lotNo: 'L-LASER-01', expiryDate: '2025-12-31', power: '50' }, -]); + const [laserConfig, setLaserConfig] = useState({ ip: '192.168.1.102', port: '8080' }); + const [laserItems, setLaserItems] = useState([ + { id: 1, templateId: 'JOB_001', lotNo: 'L-LASER-01', expiryDate: '2025-12-31', power: '50' }, + ]); + + // --- 5. HANS600S-M Section States --- + const [hansConfig, setHansConfig] = useState({ ip: '192.168.76.10', port: '45678' }); + const [hansItems, setHansItems] = useState([ + { + id: 1, + textChannel3: 'SN-HANS-001-20260117', // channel 3 (e.g. serial / text1) + textChannel4: 'BATCH-HK-TEST-OK', // channel 4 (e.g. batch / text2) + text3ObjectName: 'Text3', // EZCAD object name for channel 3 + text4ObjectName: 'Text4' // EZCAD object name for channel 4 + }, + ]); // Generic handler for inline table edits const handleItemChange = (setter: any, id: number, field: string, value: string) => { @@ -105,6 +151,7 @@ const [laserItems, setLaserItems] = useState([ } catch (e) { console.error("OnPack Error:", e); } }; + // Laser Print (Section 4 - original) const handleLaserPrint = async (row: any) => { const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; @@ -122,7 +169,6 @@ const [laserItems, setLaserItems] = useState([ const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; try { - // We'll create this endpoint in the backend next const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, @@ -132,24 +178,58 @@ const [laserItems, setLaserItems] = useState([ } catch (e) { console.error("Preview Error:", e); } }; + // HANS600S-M TCP Print (Section 5) + const handleHansPrint = async (row: any) => { + const token = localStorage.getItem("accessToken"); + const payload = { + printerIp: hansConfig.ip, + printerPort: hansConfig.port, + textChannel3: row.textChannel3, + textChannel4: row.textChannel4, + text3ObjectName: row.text3ObjectName, + text4ObjectName: row.text4ObjectName + }; + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const result = await response.text(); + if (response.ok) { + alert(`HANS600S-M Mark Success: ${result}`); + } else { + alert(`HANS600S-M Failed: ${result}`); + } + } catch (e) { + console.error("HANS600S-M Error:", e); + alert("HANS600S-M Connection Error"); + } + }; + // Layout Helper const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => ( - - - - {title} - - {children || Waiting for implementation...} - - + + + {title} + + {children || Waiting for implementation...} + ); return ( - Printer Testing Dashboard + Printer Testing - - {/* 1. TSC Section */} + + + + + + + + +
setTscConfig({...tscConfig, ip: e.target.value})} /> @@ -181,8 +261,9 @@ const [laserItems, setLaserItems] = useState([
+
- {/* 2. DataFlex Section */} +
setDfConfig({...dfConfig, ip: e.target.value})} /> @@ -214,8 +295,9 @@ const [laserItems, setLaserItems] = useState([
+
- {/* 3. OnPack Section */} +
@@ -226,8 +308,9 @@ const [laserItems, setLaserItems] = useState([
+
- {/* 4. Laser Section (HANS600S-M) */} +
setLaserConfig({...laserConfig, ip: e.target.value})} /> @@ -283,7 +366,94 @@ const [laserItems, setLaserItems] = useState([ Note: HANS Laser requires pre-saved templates on the controller.
-
+ + + +
+ + setHansConfig({...hansConfig, ip: e.target.value})} + /> + setHansConfig({...hansConfig, port: e.target.value})} + /> + + + + + + + + Ch3 Text (SN) + Ch4 Text (Batch) + Obj3 Name + Obj4 Name + Action + + + + {hansItems.map(row => ( + + + handleItemChange(setHansItems, row.id, 'textChannel3', e.target.value)} + sx={{ minWidth: 180 }} + /> + + + handleItemChange(setHansItems, row.id, 'textChannel4', e.target.value)} + sx={{ minWidth: 140 }} + /> + + + handleItemChange(setHansItems, row.id, 'text3ObjectName', e.target.value)} + size="small" + /> + + + handleItemChange(setHansItems, row.id, 'text4ObjectName', e.target.value)} + size="small" + /> + + + + + + ))} + +
+
+ + TCP Push to EZCAD3 (Ch3/Ch4 via E3_SetTextObject) | IP:192.168.76.10:45678 | Backend: /print-laser-tcp + +
+
{/* Dialog for OnPack */} setIsPrinterModalOpen(false)} fullWidth maxWidth="sm"> 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/inventory/actions.ts b/src/app/api/inventory/actions.ts index 5f09c65..fab5c80 100644 --- a/src/app/api/inventory/actions.ts +++ b/src/app/api/inventory/actions.ts @@ -152,3 +152,33 @@ export const updateInventoryLotLineQuantities = async (data: { revalidateTag("pickorder"); return result; }; + +//STOCK TRANSFER +export interface CreateStockTransferRequest { + inventoryLotLineId: number; + transferredQty: number; + warehouseId: number; +} + +export interface MessageResponse { + id: number | null; + name: string; + code: string; + type: string; + message: string | null; + errorPosition: string | null; +} + +export const createStockTransfer = async (data: CreateStockTransferRequest) => { + const result = await serverFetchJson( + `${BASE_API_URL}/stockTransferRecord/create`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("inventoryLotLines"); + revalidateTag("inventories"); + return result; +}; \ No newline at end of file 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/item/index.ts b/src/app/api/settings/item/index.ts index a30af00..cdb7cce 100644 --- a/src/app/api/settings/item/index.ts +++ b/src/app/api/settings/item/index.ts @@ -67,6 +67,7 @@ export type ItemsResult = { export type Result = { item: ItemsResult; qcChecks: ItemQc[]; + qcType?: string; }; export const fetchAllItems = cache(async () => { return serverFetchJson(`${BASE_API_URL}/items`, { diff --git a/src/app/api/settings/m18ImportTesting/actions.ts b/src/app/api/settings/m18ImportTesting/actions.ts index 9141ea6..0797b59 100644 --- a/src/app/api/settings/m18ImportTesting/actions.ts +++ b/src/app/api/settings/m18ImportTesting/actions.ts @@ -8,11 +8,15 @@ import { BASE_API_URL } from "../../../../config/api"; export interface M18ImportPoForm { modifiedDateFrom: string; modifiedDateTo: string; + dDateFrom: string; + dDateTo: string; } export interface M18ImportDoForm { modifiedDateFrom: string; modifiedDateTo: string; + dDateFrom: string; + dDateTo: string; } export interface M18ImportPqForm { @@ -49,19 +53,67 @@ export const testM18ImportDo = async (data: M18ImportDoForm) => { }; export const testM18ImportPq = async (data: M18ImportPqForm) => { + const token = localStorage.getItem("accessToken"); + return serverFetchWithNoContent(`${BASE_API_URL}/m18/pq`, { method: "POST", body: JSON.stringify(data), - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, + }); }; export const testM18ImportMasterData = async ( data: M18ImportMasterDataForm, ) => { + const token = localStorage.getItem("accessToken"); return serverFetchWithNoContent(`${BASE_API_URL}/m18/master-data`, { method: "POST", body: JSON.stringify(data), - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, }); }; + +export const triggerScheduler = async (type: 'po' | 'do1' | 'do2' | 'master-data' | 'refresh-cron') => { + try { + // IMPORTANT: 'refresh-cron' is a direct endpoint /api/scheduler/refresh-cron + // Others are /api/scheduler/trigger/{type} + const path = type === 'refresh-cron' + ? 'refresh-cron' + : `trigger/${type}`; + + const url = `${BASE_API_URL}/scheduler/${path}`; + + console.log("Fetching URL:", url); + + const response = await serverFetchWithNoContent(url, { + method: "GET", + cache: "no-store", + }); + + if (!response.ok) throw new Error(`Failed: ${response.status}`); + + return await response.text(); + } catch (error) { + console.error("Scheduler Action Error:", error); + return null; + } +}; + +export const refreshCronSchedules = async () => { + // Simply reuse the triggerScheduler logic to avoid duplication + // or call serverFetch directly as shown below: + try { + const response = await serverFetchWithNoContent(`${BASE_API_URL}/scheduler/refresh-cron`, { + method: "GET", + cache: "no-store", + }); + + if (!response.ok) throw new Error(`Failed to refresh: ${response.status}`); + + return await response.text(); + } catch (error) { + console.error("Refresh Cron Error:", error); + return "Refresh failed. Check server logs."; + } +}; \ No newline at end of file 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/app/api/settings/qcItemAll/actions.ts b/src/app/api/settings/qcItemAll/actions.ts new file mode 100644 index 0000000..e3a8dde --- /dev/null +++ b/src/app/api/settings/qcItemAll/actions.ts @@ -0,0 +1,265 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { + ItemQcCategoryMappingInfo, + QcItemInfo, + DeleteResponse, + QcCategoryResult, + ItemsResult, + QcItemResult, +} from "."; + +export interface SaveQcCategoryInputs { + id?: number; + code: string; + name: string; + description?: string; +} + +export interface SaveQcCategoryResponse { + id?: number; + code: string; + name: string; + description?: string; + errors: Record | null; +} + +export interface SaveQcItemInputs { + id?: number; + code: string; + name: string; + description?: string; +} + +export interface SaveQcItemResponse { + id?: number; + code: string; + name: string; + description?: string; + errors: Record | null; +} + +// Item and QcCategory mapping +export const getItemQcCategoryMappings = async ( + qcCategoryId?: number, + itemId?: number +): Promise => { + const params = new URLSearchParams(); + if (qcCategoryId) params.append("qcCategoryId", qcCategoryId.toString()); + if (itemId) params.append("itemId", itemId.toString()); + + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemMappings?${params.toString()}` + ); +}; + +export const saveItemQcCategoryMapping = async ( + itemId: number, + qcCategoryId: number, + type: string +): Promise => { + const params = new URLSearchParams(); + params.append("itemId", itemId.toString()); + params.append("qcCategoryId", qcCategoryId.toString()); + params.append("type", type); + + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemMapping?${params.toString()}`, + { + method: "POST", + } + ); + + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteItemQcCategoryMapping = async ( + mappingId: number +): Promise => { + await serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemMapping/${mappingId}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcItemAll"); +}; + +// QcCategory and QcItem mapping +export const getQcCategoryQcItemMappings = async ( + qcCategoryId: number +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemMappings/${qcCategoryId}` + ); +}; + +export const saveQcCategoryQcItemMapping = async ( + qcCategoryId: number, + qcItemId: number, + order: number, + description?: string +): Promise => { + const params = new URLSearchParams(); + params.append("qcCategoryId", qcCategoryId.toString()); + params.append("qcItemId", qcItemId.toString()); + params.append("order", order.toString()); + if (description) params.append("description", description); + + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemMapping?${params.toString()}`, + { + method: "POST", + } + ); + + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteQcCategoryQcItemMapping = async ( + mappingId: number +): Promise => { + await serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemMapping/${mappingId}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcItemAll"); +}; + +// Counts +export const getItemCountByQcCategory = async ( + qcCategoryId: number +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemCount/${qcCategoryId}` + ); +}; + +export const getQcItemCountByQcCategory = async ( + qcCategoryId: number +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemCount/${qcCategoryId}` + ); +}; + +// Validation +export const canDeleteQcCategory = async (id: number): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/canDeleteQcCategory/${id}` + ); +}; + +export const canDeleteQcItem = async (id: number): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/canDeleteQcItem/${id}` + ); +}; + +// Save and delete with validation +export const saveQcCategoryWithValidation = async ( + data: SaveQcCategoryInputs +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/saveQcCategory`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ); + + revalidateTag("qcCategories"); + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteQcCategoryWithValidation = async ( + id: number +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/deleteQcCategory/${id}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcCategories"); + revalidateTag("qcItemAll"); + revalidatePath("/(main)/settings/qcItemAll"); + return response; +}; + +export const saveQcItemWithValidation = async ( + data: SaveQcItemInputs +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/saveQcItem`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ); + + revalidateTag("qcItems"); + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteQcItemWithValidation = async ( + id: number +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/deleteQcItem/${id}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcItems"); + revalidateTag("qcItemAll"); + revalidatePath("/(main)/settings/qcItemAll"); + return response; +}; + +// Server actions for fetching data (to be used in client components) +export const fetchQcCategoriesForAll = async (): Promise => { + return serverFetchJson(`${BASE_API_URL}/qcCategories`, { + next: { tags: ["qcCategories"] }, + }); +}; + +export const fetchItemsForAll = async (): Promise => { + return serverFetchJson(`${BASE_API_URL}/items`, { + next: { tags: ["items"] }, + }); +}; + +export const fetchQcItemsForAll = async (): Promise => { + return serverFetchJson(`${BASE_API_URL}/qcItems`, { + next: { tags: ["qcItems"] }, + }); +}; + +// Get item by code (for Tab 0 - validate item code input) +export const getItemByCode = async (code: string): Promise => { + try { + return await serverFetchJson(`${BASE_API_URL}/qcItemAll/itemByCode/${encodeURIComponent(code)}`); + } catch (error) { + // Item not found + return null; + } +}; + + + diff --git a/src/app/api/settings/qcItemAll/index.ts b/src/app/api/settings/qcItemAll/index.ts new file mode 100644 index 0000000..e228af6 --- /dev/null +++ b/src/app/api/settings/qcItemAll/index.ts @@ -0,0 +1,101 @@ +// Type definitions that can be used in both client and server components +export interface ItemQcCategoryMappingInfo { + id: number; + itemId: number; + itemCode?: string; + itemName?: string; + qcCategoryId: number; + qcCategoryCode?: string; + qcCategoryName?: string; + type?: string; +} + +export interface QcItemInfo { + id: number; + order: number; + qcItemId: number; + code: string; + name?: string; + description?: string; +} + +export interface DeleteResponse { + success: boolean; + message?: string; + canDelete: boolean; +} + +export interface QcCategoryWithCounts { + id: number; + code: string; + name: string; + description?: string; + itemCount: number; + qcItemCount: number; +} + +export interface QcCategoryWithItemCount { + id: number; + code: string; + name: string; + description?: string; + itemCount: number; +} + +export interface QcCategoryWithQcItemCount { + id: number; + code: string; + name: string; + description?: string; + qcItemCount: number; +} + +export interface QcItemWithCounts { + id: number; + code: string; + name: string; + description?: string; + qcCategoryCount: number; +} + +// Type definitions that match the server-only types +export interface QcCategoryResult { + id: number; + code: string; + name: string; + description?: string; +} + +export interface QcItemResult { + id: number; + code: string; + name: string; + description: string; +} + +export interface ItemsResult { + id: string | number; + code: string; + name: string; + description: string | undefined; + remarks: string | undefined; + shelfLife: number | undefined; + countryOfOrigin: string | undefined; + maxQty: number | undefined; + type: string; + qcChecks: any[]; + action?: any; + fgName?: string; + excludeDate?: string; + qcCategory?: QcCategoryResult; + store_id?: string | undefined; + warehouse?: string | undefined; + area?: string | undefined; + slot?: string | undefined; + LocationCode?: string | undefined; + locationCode?: string | undefined; + isEgg?: boolean | undefined; + isFee?: boolean | undefined; + isBag?: boolean | undefined; +} + diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index e210d63..3e2e9bd 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -16,15 +16,16 @@ export interface StockIssueResult { storeLocation: string | null; requiredQty: number | null; actualPickQty: number | null; - missQty: number; - badItemQty: number; + missQty: number; + badItemQty: number; + bookQty: number; + issueQty: number; issueRemark: string | null; pickerName: string | null; handleStatus: string; handleDate: string | null; handledBy: number | null; } - export interface ExpiryItemResult { id: number; itemId: number; diff --git a/src/app/api/warehouse/client.ts b/src/app/api/warehouse/client.ts index 7b57f6a..526f09d 100644 --- a/src/app/api/warehouse/client.ts +++ b/src/app/api/warehouse/client.ts @@ -31,4 +31,25 @@ export const exportWarehouseQrCode = async (warehouseIds: number[]): Promise<{ b return { blobValue, filename }; }; + +export const fetchWarehouseListClient = async (): Promise => { + const token = localStorage.getItem("accessToken"); + + const response = await fetch(`${NEXT_PUBLIC_API_URL}/warehouse`, { + 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 warehouse list: ${response.status} ${response.statusText}`); + } + + return response.json(); +}; //test \ No newline at end of file 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/CreateItemWrapper.tsx b/src/components/CreateItem/CreateItemWrapper.tsx index 095eff8..343e6bf 100644 --- a/src/components/CreateItem/CreateItemWrapper.tsx +++ b/src/components/CreateItem/CreateItemWrapper.tsx @@ -51,6 +51,7 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { qcChecks: qcChecks, qcChecks_active: activeRows, qcCategoryId: item.qcCategory?.id, + qcType: result.qcType, store_id: item?.store_id, warehouse: item?.warehouse, area: item?.area, 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/components/InventorySearch/InventoryLotLineTable.tsx b/src/components/InventorySearch/InventoryLotLineTable.tsx index b325aff..14d0ba4 100644 --- a/src/components/InventorySearch/InventoryLotLineTable.tsx +++ b/src/components/InventorySearch/InventoryLotLineTable.tsx @@ -15,7 +15,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { Autocomplete } from "@mui/material"; import { WarehouseResult } from "@/app/api/warehouse"; import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; -import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; +import { createStockTransfer } from "@/app/api/inventory/actions"; interface Props { inventoryLotLines: InventoryLotLineResult[] | null; @@ -31,7 +31,7 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); const [selectedLotLine, setSelectedLotLine] = useState(null); const [startLocation, setStartLocation] = useState(""); - const [targetLocation, setTargetLocation] = useState(""); + const [targetLocation, setTargetLocation] = useState(null); // Store warehouse ID instead of code const [targetLocationInput, setTargetLocationInput] = useState(""); const [qtyToBeTransferred, setQtyToBeTransferred] = useState(0); const [warehouses, setWarehouses] = useState([]); @@ -65,7 +65,7 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr setSelectedLotLine(lotLine); setStockTransferModalOpen(true); setStartLocation(lotLine.warehouse.code || ""); - setTargetLocation(""); + setTargetLocation(null); setTargetLocationInput(""); setQtyToBeTransferred(0); }, @@ -188,34 +188,46 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr ); const handleCloseStockTransferModal = useCallback(() => { - setStockTransferModalOpen(false); - setSelectedLotLine(null); - setStartLocation(""); - setTargetLocation(""); - setTargetLocationInput(""); - setQtyToBeTransferred(0); + setStockTransferModalOpen(false); + setSelectedLotLine(null); + setStartLocation(""); + setTargetLocation(null); + setTargetLocationInput(""); + setQtyToBeTransferred(0); }, []); const handleSubmitStockTransfer = useCallback(async () => { - try { - setIsUploading(true); - - // Decrease the inQty (availableQty) in the source inventory lot line + if (!selectedLotLine || !targetLocation || qtyToBeTransferred <= 0) { + return; + } + try { + setIsUploading(true); + + const request = { + inventoryLotLineId: selectedLotLine.id, + transferredQty: qtyToBeTransferred, + warehouseId: targetLocation, // targetLocation now contains warehouse ID + }; - // TODO: Add logic to increase qty in target location warehouse - - alert(t("Stock transfer successful")); - handleCloseStockTransferModal(); - - // TODO: Refresh the inventory lot lines list - } catch (error: any) { - console.error("Error transferring stock:", error); - alert(error?.message || t("Failed to transfer stock. Please try again.")); - } finally { - setIsUploading(false); + const response = await createStockTransfer(request); + + if (response && response.type === "success") { + alert(t("Stock transfer successful")); + handleCloseStockTransferModal(); + + // Refresh the inventory lot lines list + window.location.reload(); // Or use your preferred refresh method + } else { + throw new Error(response?.message || t("Failed to transfer stock")); } - }, [selectedLotLine, targetLocation, qtyToBeTransferred, originalQty, handleCloseStockTransferModal, setIsUploading, t]); + } catch (error: any) { + console.error("Error transferring stock:", error); + alert(error?.message || t("Failed to transfer stock. Please try again.")); + } finally { + setIsUploading(false); + } + }, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t]); return <> {inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")} @@ -276,55 +288,55 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr w.code !== startLocation)} - getOptionLabel={(option) => option.code || ""} - value={targetLocation ? warehouses.find(w => w.code === targetLocation) || null : null} - inputValue={targetLocationInput} - onInputChange={(event, newInputValue) => { - setTargetLocationInput(newInputValue); - if (targetLocation && newInputValue !== targetLocation) { - setTargetLocation(""); - } - }} - onChange={(event, newValue) => { - if (newValue) { - setTargetLocation(newValue.code); - setTargetLocationInput(newValue.code); - } else { - setTargetLocation(""); - setTargetLocationInput(""); - } - }} - filterOptions={(options, { inputValue }) => { - if (!inputValue || inputValue.trim() === "") return options; - const searchTerm = inputValue.toLowerCase().trim(); - return options.filter((option) => - (option.code || "").toLowerCase().includes(searchTerm) || - (option.name || "").toLowerCase().includes(searchTerm) || - (option.description || "").toLowerCase().includes(searchTerm) - ); + options={warehouses.filter(w => w.code !== startLocation)} + getOptionLabel={(option) => option.code || ""} + value={targetLocation ? warehouses.find(w => w.id === targetLocation) || null : null} + inputValue={targetLocationInput} + onInputChange={(event, newInputValue) => { + setTargetLocationInput(newInputValue); + if (targetLocation && newInputValue !== warehouses.find(w => w.id === targetLocation)?.code) { + setTargetLocation(null); + } + }} + onChange={(event, newValue) => { + if (newValue) { + setTargetLocation(newValue.id); + setTargetLocationInput(newValue.code); + } else { + setTargetLocation(null); + setTargetLocationInput(""); + } + }} + filterOptions={(options, { inputValue }) => { + if (!inputValue || inputValue.trim() === "") return options; + const searchTerm = inputValue.toLowerCase().trim(); + return options.filter((option) => + (option.code || "").toLowerCase().includes(searchTerm) || + (option.name || "").toLowerCase().includes(searchTerm) || + (option.description || "").toLowerCase().includes(searchTerm) + ); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + autoHighlight={false} + autoSelect={false} + clearOnBlur={false} + renderOption={(props, option) => ( +
  • + {option.code} +
  • + )} + renderInput={(params) => ( + option.code === value.code} - autoHighlight={false} - autoSelect={false} - clearOnBlur={false} - renderOption={(props, option) => ( -
  • - {option.code} -
  • - )} - renderInput={(params) => ( - - )} + /> + )} />
    diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index 9da740b..112a95d 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -124,12 +124,13 @@ const ItemsSearch: React.FC = ({ items }) => { ); useEffect(() => { - refetchData(filterObj); + // Only refetch when paging changes AND we have already searched (filterObj has been set by search) + if (Object.keys(filterObj).length > 0 || filteredItems.length > 0) { + refetchData(filterObj); + } }, [ - filterObj, pagingController.pageNum, pagingController.pageSize, - refetchData, ]); const columns = useMemo[]>( @@ -181,25 +182,20 @@ const ItemsSearch: React.FC = ({ items }) => { ); const onReset = useCallback(() => { - setFilteredItems(items); - }, [items]); + setFilteredItems([]); + setFilterObj({}); + setTotalCount(0); + }, []); return ( <> { - // setFilteredItems( - // items.filter((pm) => { - // return ( - // pm.code.toLowerCase().includes(query.code.toLowerCase()) && - // pm.name.toLowerCase().includes(query.name.toLowerCase()) - // ); - // }) - // ); setFilterObj({ ...query, }); + refetchData(query); }} onReset={onReset} /> diff --git a/src/components/M18ImportTesting/M18ImportDo.tsx b/src/components/M18ImportTesting/M18ImportDo.tsx index ac00212..cd5c71a 100644 --- a/src/components/M18ImportTesting/M18ImportDo.tsx +++ b/src/components/M18ImportTesting/M18ImportDo.tsx @@ -70,7 +70,7 @@ const M18ImportDo: React.FC = ({}) => { = ({}) => { // }} render={({ field, fieldState: { error } }) => ( handleDateTimePickerOnChange(newValue, field.onChange) @@ -104,7 +104,7 @@ const M18ImportDo: React.FC = ({}) => { = ({}) => { // }} render={({ field, fieldState: { error } }) => ( handleDateTimePickerOnChange(newValue, field.onChange) diff --git a/src/components/M18ImportTesting/M18ImportPo.tsx b/src/components/M18ImportTesting/M18ImportPo.tsx index 463dc4e..84b1f1e 100644 --- a/src/components/M18ImportTesting/M18ImportPo.tsx +++ b/src/components/M18ImportTesting/M18ImportPo.tsx @@ -70,7 +70,7 @@ const M18ImportPo: React.FC = ({}) => { = ({}) => { // }} render={({ field, fieldState: { error } }) => ( handleDateTimePickerOnChange(newValue, field.onChange) @@ -104,7 +104,7 @@ const M18ImportPo: React.FC = ({}) => { = ({}) => { // }} render={({ field, fieldState: { error } }) => ( handleDateTimePickerOnChange(newValue, field.onChange) diff --git a/src/components/M18ImportTesting/M18ImportTesting.tsx b/src/components/M18ImportTesting/M18ImportTesting.tsx index 0b46d54..f2ddaad 100644 --- a/src/components/M18ImportTesting/M18ImportTesting.tsx +++ b/src/components/M18ImportTesting/M18ImportTesting.tsx @@ -8,7 +8,7 @@ import { testM18ImportMasterData, testM18ImportDo, } from "@/app/api/settings/m18ImportTesting/actions"; -import { Card, CardContent, Grid, Stack, Typography } from "@mui/material"; +import { Card, CardContent, Grid, Stack, Typography, Button } from "@mui/material"; import React, { BaseSyntheticEvent, FormEvent, @@ -22,6 +22,8 @@ import M18ImportPq from "./M18ImportPq"; import { dateTimeStringToDayjs } from "@/app/utils/formatUtil"; import M18ImportMasterData from "./M18ImportMasterData"; import M18ImportDo from "./M18ImportDo"; +import { PlayArrow, Refresh as RefreshIcon } from "@mui/icons-material"; +import { triggerScheduler, refreshCronSchedules } from "@/app/api/settings/m18ImportTesting/actions"; interface Props {} @@ -166,9 +168,80 @@ const M18ImportTesting: React.FC = ({}) => { // [], // ); + const handleManualTrigger = async (type: any) => { + setIsLoading(true); + setLoadingType(`Manual ${type}`); + try { + const result = await triggerScheduler(type); + if (result) alert(result); + } catch (error) { + console.error(error); + alert("Trigger failed. Check server logs."); + } finally { + setIsLoading(false); + } + }; + + const handleRefreshSchedules = async () => { + // Re-use the manual trigger logic which we know works + await handleManualTrigger('refresh-cron'); + }; + return ( + {t("Manual Scheduler Triggers")} + + + + + + + + +
    + + + {t("Status: ")} + {isLoading ? t(`Processing ${loadingType}...`) : t("Ready")} + + {t("Status: ")} {isLoading ? t(`Importing ${loadingType}...`) : t("Ready to import")} diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 966a7ed..4fe6c12 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -247,7 +247,7 @@ const NavigationContent: React.FC = () => { icon: , label: "PS", path: "/ps", - requiredAbility: AUTH.TESTING, + requiredAbility: [AUTH.TESTING, AUTH.ADMIN], isHidden: false, }, { @@ -353,9 +353,14 @@ const NavigationContent: React.FC = () => { path: "/settings/user", }, { - icon: , - label: "QR Code Handle", - path: "/settings/qrCodeHandle", + icon: , + label: "QC Check Template", + path: "/settings/user", + }, + { + icon: , + label: "QC Item All", + path: "/settings/qcItemAll", }, // { // icon: , diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 9ed33f6..99d485a 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -484,7 +484,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { }, { field: "reqQty", - headerName: t("Req. Qty"), + headerName: t("Bom Req. Qty"), flex: 0.7, align: "right", headerAlign: "right", diff --git a/src/components/QcItemAll/QcItemAllTabs.tsx b/src/components/QcItemAll/QcItemAllTabs.tsx new file mode 100644 index 0000000..47ed9b5 --- /dev/null +++ b/src/components/QcItemAll/QcItemAllTabs.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState, ReactNode, useEffect } from "react"; +import { Box, Tabs, Tab } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useSearchParams, useRouter } from "next/navigation"; + +interface TabPanelProps { + children?: ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +interface QcItemAllTabsProps { + tab0Content: ReactNode; + tab1Content: ReactNode; + tab2Content: ReactNode; + tab3Content: ReactNode; +} + +const QcItemAllTabs: React.FC = ({ + tab0Content, + tab1Content, + tab2Content, + tab3Content, +}) => { + const { t } = useTranslation("qcItemAll"); + const searchParams = useSearchParams(); + const router = useRouter(); + + const getInitialTab = () => { + const tab = searchParams.get("tab"); + if (tab === "1") return 1; + if (tab === "2") return 2; + if (tab === "3") return 3; + return 0; + }; + + const [currentTab, setCurrentTab] = useState(getInitialTab); + + useEffect(() => { + const tab = searchParams.get("tab"); + const tabIndex = tab ? parseInt(tab, 10) : 0; + setCurrentTab(tabIndex); + }, [searchParams]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue); + const params = new URLSearchParams(searchParams.toString()); + if (newValue === 0) { + params.delete("tab"); + } else { + params.set("tab", newValue.toString()); + } + router.push(`?${params.toString()}`, { scroll: false }); + }; + + return ( + + + + + + + + + + + + {tab0Content} + + + + {tab1Content} + + + + {tab2Content} + + + + {tab3Content} + + + ); +}; + +export default QcItemAllTabs; + diff --git a/src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx b/src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx new file mode 100644 index 0000000..585480d --- /dev/null +++ b/src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + IconButton, + CircularProgress, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Add, Delete, Edit } from "@mui/icons-material"; +import SearchBox, { Criterion } from "../SearchBox/SearchBox"; +import SearchResults, { Column } from "../SearchResults/SearchResults"; +import { + saveItemQcCategoryMapping, + deleteItemQcCategoryMapping, + getItemQcCategoryMappings, + fetchQcCategoriesForAll, + fetchItemsForAll, + getItemByCode, +} from "@/app/api/settings/qcItemAll/actions"; +import { + QcCategoryResult, + ItemsResult, +} from "@/app/api/settings/qcItemAll"; +import { ItemQcCategoryMappingInfo } from "@/app/api/settings/qcItemAll"; +import { + deleteDialog, + errorDialogWithContent, + submitDialog, + successDialog, +} from "../Swal/CustomAlerts"; + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const Tab0ItemQcCategoryMapping: React.FC = () => { + const { t } = useTranslation("qcItemAll"); + const [qcCategories, setQcCategories] = useState([]); + const [filteredQcCategories, setFilteredQcCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + const [mappings, setMappings] = useState([]); + const [openDialog, setOpenDialog] = useState(false); + const [openAddDialog, setOpenAddDialog] = useState(false); + const [itemCode, setItemCode] = useState(""); + const [validatedItem, setValidatedItem] = useState(null); + const [itemCodeError, setItemCodeError] = useState(""); + const [validatingItemCode, setValidatingItemCode] = useState(false); + const [selectedType, setSelectedType] = useState("IQC"); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + try { + // Only load categories list (same as Tab 2) - fast! + const categories = await fetchQcCategoriesForAll(); + setQcCategories(categories || []); + setFilteredQcCategories(categories || []); + } catch (error) { + console.error("Tab0: Error loading data:", error); + setQcCategories([]); + setFilteredQcCategories([]); + if (error instanceof Error) { + errorDialogWithContent(t("Error"), error.message, t); + } + } finally { + setLoading(false); + } + }; + loadData(); + }, []); + + const handleViewMappings = useCallback(async (category: QcCategoryResult) => { + setSelectedCategory(category); + const mappingData = await getItemQcCategoryMappings(category.id); + setMappings(mappingData); + setOpenDialog(true); + }, []); + + const handleAddMapping = useCallback(() => { + if (!selectedCategory) return; + setItemCode(""); + setValidatedItem(null); + setItemCodeError(""); + setOpenAddDialog(true); + }, [selectedCategory]); + + const handleItemCodeChange = useCallback(async (code: string) => { + setItemCode(code); + setValidatedItem(null); + setItemCodeError(""); + + if (!code || code.trim() === "") { + return; + } + + setValidatingItemCode(true); + try { + const item = await getItemByCode(code.trim()); + if (item) { + setValidatedItem(item); + setItemCodeError(""); + } else { + setValidatedItem(null); + setItemCodeError(t("Item code not found")); + } + } catch (error) { + setValidatedItem(null); + setItemCodeError(t("Error validating item code")); + } finally { + setValidatingItemCode(false); + } + }, [t]); + + const handleSaveMapping = useCallback(async () => { + if (!selectedCategory || !validatedItem) return; + + await submitDialog(async () => { + try { + await saveItemQcCategoryMapping( + validatedItem.id as number, + selectedCategory.id, + selectedType + ); + // Close add dialog first + setOpenAddDialog(false); + setItemCode(""); + setValidatedItem(null); + setItemCodeError(""); + // Reload mappings to update the view + const mappingData = await getItemQcCategoryMappings(selectedCategory.id); + setMappings(mappingData); + // Show success message after closing dialogs + await successDialog(t("Submit Success"), t); + // Keep the view dialog open to show updated data + } catch (error) { + errorDialogWithContent(t("Submit Error"), String(error), t); + } + }, t); + }, [selectedCategory, validatedItem, selectedType, t]); + + const handleDeleteMapping = useCallback( + async (mappingId: number) => { + if (!selectedCategory) return; + + deleteDialog(async () => { + try { + await deleteItemQcCategoryMapping(mappingId); + await successDialog(t("Delete Success"), t); + // Reload mappings + const mappingData = await getItemQcCategoryMappings(selectedCategory.id); + setMappings(mappingData); + // No need to reload categories list - it doesn't change + } catch (error) { + errorDialogWithContent(t("Delete Error"), String(error), t); + } + }, t); + }, + [selectedCategory, t] + ); + + const typeOptions = ["IQC", "IPQC", "OQC", "FQC"]; + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Name"), paramName: "name", type: "text" }, + ], + [t] + ); + + const onReset = useCallback(() => { + setFilteredQcCategories(qcCategories); + }, [qcCategories]); + + const columnWidthSx = (width = "10%") => { + return { width: width, whiteSpace: "nowrap" }; + }; + + const columns = useMemo[]>( + () => [ + { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") }, + { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") }, + { + name: "id", + label: t("Actions"), + onClick: (category) => handleViewMappings(category), + buttonIcon: , + buttonIcons: {} as any, + sx: columnWidthSx("10%"), + }, + ], + [t, handleViewMappings] + ); + + if (loading) { + return ( + + + + ); + } + + return ( + + { + setFilteredQcCategories( + qcCategories.filter( + (qc) => + (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) && + (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase())) + ) + ); + }} + onReset={onReset} + /> + + items={filteredQcCategories} + columns={columns} + /> + + {/* View Mappings Dialog */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {t("Mapping Details")} - {selectedCategory?.name} + + + + + + + + + + + {t("Item Code")} + {t("Item Name")} + {t("Type")} + {t("Actions")} + + + + {mappings.length === 0 ? ( + + + {t("No mappings found")} + + + ) : ( + mappings.map((mapping) => ( + + {mapping.itemCode} + {mapping.itemName} + {mapping.type} + + handleDeleteMapping(mapping.id)} + > + + + + + )) + )} + +
    +
    +
    +
    + + + +
    + + {/* Add Mapping Dialog */} + setOpenAddDialog(false)} maxWidth="sm" fullWidth> + {t("Add Mapping")} + + + handleItemCodeChange(e.target.value)} + error={!!itemCodeError} + helperText={itemCodeError || (validatedItem ? `${validatedItem.code} - ${validatedItem.name}` : t("Enter item code to validate"))} + fullWidth + disabled={validatingItemCode} + InputProps={{ + endAdornment: validatingItemCode ? : null, + }} + /> + setSelectedType(e.target.value)} + SelectProps={{ + native: true, + }} + fullWidth + > + {typeOptions.map((type) => ( + + ))} + + + + + + + + +
    + ); +}; + +export default Tab0ItemQcCategoryMapping; + diff --git a/src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx b/src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx new file mode 100644 index 0000000..5544dfb --- /dev/null +++ b/src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx @@ -0,0 +1,304 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Autocomplete, + CircularProgress, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Add, Delete, Edit } from "@mui/icons-material"; +import SearchBox, { Criterion } from "../SearchBox/SearchBox"; +import SearchResults, { Column } from "../SearchResults/SearchResults"; +import { + saveQcCategoryQcItemMapping, + deleteQcCategoryQcItemMapping, + getQcCategoryQcItemMappings, + fetchQcCategoriesForAll, + fetchQcItemsForAll, +} from "@/app/api/settings/qcItemAll/actions"; +import { + QcCategoryResult, + QcItemResult, +} from "@/app/api/settings/qcItemAll"; +import { QcItemInfo } from "@/app/api/settings/qcItemAll"; +import { + deleteDialog, + errorDialogWithContent, + submitDialog, + successDialog, +} from "../Swal/CustomAlerts"; + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const Tab1QcCategoryQcItemMapping: React.FC = () => { + const { t } = useTranslation("qcItemAll"); + const [qcCategories, setQcCategories] = useState([]); + const [filteredQcCategories, setFilteredQcCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + const [mappings, setMappings] = useState([]); + const [openDialog, setOpenDialog] = useState(false); + const [openAddDialog, setOpenAddDialog] = useState(false); + const [qcItems, setQcItems] = useState([]); + const [selectedQcItem, setSelectedQcItem] = useState(null); + const [order, setOrder] = useState(0); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + try { + // Only load categories list (same as Tab 2) - fast! + const categories = await fetchQcCategoriesForAll(); + setQcCategories(categories || []); + setFilteredQcCategories(categories || []); + } catch (error) { + console.error("Error loading data:", error); + setQcCategories([]); // Ensure it's always an array + setFilteredQcCategories([]); + } finally { + setLoading(false); + } + }; + loadData(); + }, []); + + const handleViewMappings = useCallback(async (category: QcCategoryResult) => { + setSelectedCategory(category); + // Load mappings when user clicks View (lazy loading) + const mappingData = await getQcCategoryQcItemMappings(category.id); + setMappings(mappingData); + setOpenDialog(true); + }, []); + + const handleAddMapping = useCallback(async () => { + if (!selectedCategory) return; + // Load qc items list when opening add dialog + try { + const itemsData = await fetchQcItemsForAll(); + setQcItems(itemsData); + } catch (error) { + console.error("Error loading qc items:", error); + } + setOpenAddDialog(true); + setOrder(0); + setSelectedQcItem(null); + }, [selectedCategory]); + + const handleSaveMapping = useCallback(async () => { + if (!selectedCategory || !selectedQcItem) return; + + await submitDialog(async () => { + try { + await saveQcCategoryQcItemMapping( + selectedCategory.id, + selectedQcItem.id, + order, + undefined // No description needed - qcItem already has description + ); + // Close add dialog first + setOpenAddDialog(false); + setSelectedQcItem(null); + setOrder(0); + // Reload mappings to update the view + const mappingData = await getQcCategoryQcItemMappings(selectedCategory.id); + setMappings(mappingData); + // Show success message after closing dialogs + await successDialog(t("Submit Success"), t); + // Keep the view dialog open to show updated data + } catch (error) { + errorDialogWithContent(t("Submit Error"), String(error), t); + } + }, t); + }, [selectedCategory, selectedQcItem, order, t]); + + const handleDeleteMapping = useCallback( + async (mappingId: number) => { + if (!selectedCategory) return; + + deleteDialog(async () => { + try { + await deleteQcCategoryQcItemMapping(mappingId); + await successDialog(t("Delete Success"), t); + // Reload mappings + const mappingData = await getQcCategoryQcItemMappings(selectedCategory.id); + setMappings(mappingData); + // No need to reload categories list - it doesn't change + } catch (error) { + errorDialogWithContent(t("Delete Error"), String(error), t); + } + }, t); + }, + [selectedCategory, t] + ); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Name"), paramName: "name", type: "text" }, + ], + [t] + ); + + const onReset = useCallback(() => { + setFilteredQcCategories(qcCategories); + }, [qcCategories]); + + const columnWidthSx = (width = "10%") => { + return { width: width, whiteSpace: "nowrap" }; + }; + + const columns = useMemo[]>( + () => [ + { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") }, + { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") }, + { + name: "id", + label: t("Actions"), + onClick: (category) => handleViewMappings(category), + buttonIcon: , + buttonIcons: {} as any, + sx: columnWidthSx("10%"), + }, + ], + [t, handleViewMappings] + ); + + return ( + + { + setFilteredQcCategories( + qcCategories.filter( + (qc) => + (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) && + (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase())) + ) + ); + }} + onReset={onReset} + /> + + items={filteredQcCategories} + columns={columns} + /> + + {/* View Mappings Dialog */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {t("Association Details")} - {selectedCategory?.name} + + + + + + + + + + + {t("Order")} + {t("Qc Item Code")} + {t("Qc Item Name")} + {t("Description")} + {t("Actions")} + + + + {mappings.length === 0 ? ( + + + {t("No associations found")} + + + ) : ( + mappings.map((mapping) => ( + + {mapping.order} + {mapping.code} + {mapping.name} + {mapping.description || "-"} + + handleDeleteMapping(mapping.id)} + > + + + + + )) + )} + +
    +
    +
    +
    + + + +
    + + {/* Add Mapping Dialog */} + setOpenAddDialog(false)} maxWidth="sm" fullWidth> + {t("Add Association")} + + + `${option.code} - ${option.name}`} + value={selectedQcItem} + onChange={(_, newValue) => setSelectedQcItem(newValue)} + renderInput={(params) => ( + + )} + /> + setOrder(parseInt(e.target.value) || 0)} + fullWidth + /> + + + + + + + +
    + ); +}; + +export default Tab1QcCategoryQcItemMapping; + diff --git a/src/components/QcItemAll/Tab2QcCategoryManagement.tsx b/src/components/QcItemAll/Tab2QcCategoryManagement.tsx new file mode 100644 index 0000000..5e0992c --- /dev/null +++ b/src/components/QcItemAll/Tab2QcCategoryManagement.tsx @@ -0,0 +1,226 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import EditNote from "@mui/icons-material/EditNote"; +import { fetchQcCategoriesForAll } from "@/app/api/settings/qcItemAll/actions"; +import { QcCategoryResult } from "@/app/api/settings/qcItemAll"; +import { + deleteDialog, + errorDialogWithContent, + submitDialog, + successDialog, +} from "../Swal/CustomAlerts"; +import { + deleteQcCategoryWithValidation, + canDeleteQcCategory, + saveQcCategoryWithValidation, + SaveQcCategoryInputs, +} from "@/app/api/settings/qcItemAll/actions"; +import Delete from "@mui/icons-material/Delete"; +import { Add } from "@mui/icons-material"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material"; +import QcCategoryDetails from "../QcCategorySave/QcCategoryDetails"; +import { FormProvider, useForm } from "react-hook-form"; + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const Tab2QcCategoryManagement: React.FC = () => { + const { t } = useTranslation("qcItemAll"); + const [qcCategories, setQcCategories] = useState([]); + const [filteredQcCategories, setFilteredQcCategories] = useState([]); + const [openDialog, setOpenDialog] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + + useEffect(() => { + loadCategories(); + }, []); + + const loadCategories = async () => { + const categories = await fetchQcCategoriesForAll(); + setQcCategories(categories); + setFilteredQcCategories(categories); + }; + + const formProps = useForm({ + defaultValues: { + code: "", + name: "", + description: "", + }, + }); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Name"), paramName: "name", type: "text" }, + ], + [t] + ); + + const onReset = useCallback(() => { + setFilteredQcCategories(qcCategories); + }, [qcCategories]); + + const handleEdit = useCallback((qcCategory: QcCategoryResult) => { + setEditingCategory(qcCategory); + formProps.reset({ + id: qcCategory.id, + code: qcCategory.code, + name: qcCategory.name, + description: qcCategory.description || "", + }); + setOpenDialog(true); + }, [formProps]); + + const handleAdd = useCallback(() => { + setEditingCategory(null); + formProps.reset({ + code: "", + name: "", + description: "", + }); + setOpenDialog(true); + }, [formProps]); + + const handleSubmit = useCallback(async (data: SaveQcCategoryInputs) => { + await submitDialog(async () => { + try { + const response = await saveQcCategoryWithValidation(data); + if (response.errors) { + let errorContents = ""; + for (const [key, value] of Object.entries(response.errors)) { + formProps.setError(key as keyof SaveQcCategoryInputs, { + type: "custom", + message: value, + }); + errorContents = errorContents + t(value) + "
    "; + } + errorDialogWithContent(t("Submit Error"), errorContents, t); + } else { + await successDialog(t("Submit Success"), t); + setOpenDialog(false); + await loadCategories(); + } + } catch (error) { + errorDialogWithContent(t("Submit Error"), String(error), t); + } + }, t); + }, [formProps, t]); + + const handleDelete = useCallback(async (qcCategory: QcCategoryResult) => { + // Check if can delete first + const canDelete = await canDeleteQcCategory(qcCategory.id); // This is a server action, token handled server-side + + if (!canDelete) { + errorDialogWithContent( + t("Cannot Delete"), + t("Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.").replace("{itemCount}", "some").replace("{qcItemCount}", "some"), + t + ); + return; + } + + deleteDialog(async () => { + try { + const response = await deleteQcCategoryWithValidation(qcCategory.id); + if (!response.success || !response.canDelete) { + errorDialogWithContent( + t("Delete Error"), + response.message || t("Cannot Delete"), + t + ); + } else { + await successDialog(t("Delete Success"), t); + await loadCategories(); + } + } catch (error) { + errorDialogWithContent(t("Delete Error"), String(error), t); + } + }, t); + }, [t]); + + const columnWidthSx = (width = "10%") => { + return { width: width, whiteSpace: "nowrap" }; + }; + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: handleEdit, + buttonIcon: , + sx: columnWidthSx("5%"), + }, + { name: "code", label: t("Code"), sx: columnWidthSx("15%") }, + { name: "name", label: t("Name"), sx: columnWidthSx("30%") }, + { + name: "id", + label: t("Delete"), + onClick: handleDelete, + buttonIcon: , + buttonColor: "error", + sx: columnWidthSx("5%"), + }, + ], + [t, handleEdit, handleDelete] + ); + + return ( + <> + + + + { + setFilteredQcCategories( + qcCategories.filter( + (qc) => + (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) && + (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase())) + ) + ); + }} + onReset={onReset} + /> + + items={filteredQcCategories} + columns={columns} + /> + + {/* Add/Edit Dialog */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {editingCategory ? t("Edit Qc Category") : t("Create Qc Category")} + + +
    + + + + + + + +
    +
    +
    + + ); +}; + +export default Tab2QcCategoryManagement; + diff --git a/src/components/QcItemAll/Tab3QcItemManagement.tsx b/src/components/QcItemAll/Tab3QcItemManagement.tsx new file mode 100644 index 0000000..33591fc --- /dev/null +++ b/src/components/QcItemAll/Tab3QcItemManagement.tsx @@ -0,0 +1,226 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import EditNote from "@mui/icons-material/EditNote"; +import { fetchQcItemsForAll } from "@/app/api/settings/qcItemAll/actions"; +import { QcItemResult } from "@/app/api/settings/qcItemAll"; +import { + deleteDialog, + errorDialogWithContent, + submitDialog, + successDialog, +} from "../Swal/CustomAlerts"; +import { + deleteQcItemWithValidation, + canDeleteQcItem, + saveQcItemWithValidation, + SaveQcItemInputs, +} from "@/app/api/settings/qcItemAll/actions"; +import Delete from "@mui/icons-material/Delete"; +import { Add } from "@mui/icons-material"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material"; +import QcItemDetails from "../QcItemSave/QcItemDetails"; +import { FormProvider, useForm } from "react-hook-form"; + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const Tab3QcItemManagement: React.FC = () => { + const { t } = useTranslation("qcItemAll"); + const [qcItems, setQcItems] = useState([]); + const [filteredQcItems, setFilteredQcItems] = useState([]); + const [openDialog, setOpenDialog] = useState(false); + const [editingItem, setEditingItem] = useState(null); + + useEffect(() => { + loadItems(); + }, []); + + const loadItems = async () => { + const items = await fetchQcItemsForAll(); + setQcItems(items); + setFilteredQcItems(items); + }; + + const formProps = useForm({ + defaultValues: { + code: "", + name: "", + description: "", + }, + }); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Name"), paramName: "name", type: "text" }, + ], + [t] + ); + + const onReset = useCallback(() => { + setFilteredQcItems(qcItems); + }, [qcItems]); + + const handleEdit = useCallback((qcItem: QcItemResult) => { + setEditingItem(qcItem); + formProps.reset({ + id: qcItem.id, + code: qcItem.code, + name: qcItem.name, + description: qcItem.description || "", + }); + setOpenDialog(true); + }, [formProps]); + + const handleAdd = useCallback(() => { + setEditingItem(null); + formProps.reset({ + code: "", + name: "", + description: "", + }); + setOpenDialog(true); + }, [formProps]); + + const handleSubmit = useCallback(async (data: SaveQcItemInputs) => { + await submitDialog(async () => { + try { + const response = await saveQcItemWithValidation(data); + if (response.errors) { + let errorContents = ""; + for (const [key, value] of Object.entries(response.errors)) { + formProps.setError(key as keyof SaveQcItemInputs, { + type: "custom", + message: value, + }); + errorContents = errorContents + t(value) + "
    "; + } + errorDialogWithContent(t("Submit Error"), errorContents, t); + } else { + await successDialog(t("Submit Success"), t); + setOpenDialog(false); + await loadItems(); + } + } catch (error) { + errorDialogWithContent(t("Submit Error"), String(error), t); + } + }, t); + }, [formProps, t]); + + const handleDelete = useCallback(async (qcItem: QcItemResult) => { + // Check if can delete first + const canDelete = await canDeleteQcItem(qcItem.id); + + if (!canDelete) { + errorDialogWithContent( + t("Cannot Delete"), + t("Cannot delete QcItem. It is linked to one or more QcCategories."), + t + ); + return; + } + + deleteDialog(async () => { + try { + const response = await deleteQcItemWithValidation(qcItem.id); + if (!response.success || !response.canDelete) { + errorDialogWithContent( + t("Delete Error"), + response.message || t("Cannot Delete"), + t + ); + } else { + await successDialog(t("Delete Success"), t); + await loadItems(); + } + } catch (error) { + errorDialogWithContent(t("Delete Error"), String(error), t); + } + }, t); + }, [t]); + + const columnWidthSx = (width = "10%") => { + return { width: width, whiteSpace: "nowrap" }; + }; + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: handleEdit, + buttonIcon: , + sx: columnWidthSx("150px"), + }, + { name: "code", label: t("Code"), sx: columnWidthSx() }, + { name: "name", label: t("Name"), sx: columnWidthSx() }, + { name: "description", label: t("Description") }, + { + name: "id", + label: t("Delete"), + onClick: handleDelete, + buttonIcon: , + buttonColor: "error", + }, + ], + [t, handleEdit, handleDelete] + ); + + return ( + <> + + + + { + setFilteredQcItems( + qcItems.filter( + (qi) => + (!query.code || qi.code.toLowerCase().includes(query.code.toLowerCase())) && + (!query.name || qi.name.toLowerCase().includes(query.name.toLowerCase())) + ) + ); + }} + onReset={onReset} + /> + + items={filteredQcItems} + columns={columns} + /> + + {/* Add/Edit Dialog */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {editingItem ? t("Edit Qc Item") : t("Create Qc Item")} + + +
    + + + + + + + +
    +
    +
    + + ); +}; + +export default Tab3QcItemManagement; + diff --git a/src/components/Shop/Shop.tsx b/src/components/Shop/Shop.tsx index 07b07cb..85a6601 100644 --- a/src/components/Shop/Shop.tsx +++ b/src/components/Shop/Shop.tsx @@ -303,12 +303,6 @@ const Shop: React.FC = () => { } }, [searchParams]); - useEffect(() => { - if (activeTab === 0) { - fetchAllShops(); - } - }, [activeTab]); - const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setActiveTab(newValue); // Update URL to reflect the selected tab diff --git a/src/components/Shop/TruckLane.tsx b/src/components/Shop/TruckLane.tsx index efe0bc5..146318f 100644 --- a/src/components/Shop/TruckLane.tsx +++ b/src/components/Shop/TruckLane.tsx @@ -30,7 +30,7 @@ import { } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; import SaveIcon from "@mui/icons-material/Save"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; @@ -50,7 +50,7 @@ const TruckLane: React.FC = () => { const { t } = useTranslation("common"); const router = useRouter(); const [truckData, setTruckData] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [filters, setFilters] = useState>({}); const [page, setPage] = useState(0); @@ -65,32 +65,6 @@ const TruckLane: React.FC = () => { const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(""); - useEffect(() => { - const fetchTruckLanes = async () => { - setLoading(true); - setError(null); - try { - const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; - // Get unique truckLanceCodes only - const uniqueCodes = new Map(); - (data || []).forEach((truck) => { - const code = String(truck.truckLanceCode || "").trim(); - if (code && !uniqueCodes.has(code)) { - uniqueCodes.set(code, truck); - } - }); - setTruckData(Array.from(uniqueCodes.values())); - } catch (err: any) { - console.error("Failed to load truck lanes:", err); - setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); - } finally { - setLoading(false); - } - }; - - fetchTruckLanes(); - }, [t]); - // Client-side filtered rows (contains-matching) const filteredRows = useMemo(() => { const fKeys = Object.keys(filters).filter((k) => String(filters[k] ?? "").trim() !== ""); @@ -125,9 +99,27 @@ const TruckLane: React.FC = () => { return filteredRows.slice(startIndex, startIndex + rowsPerPage); }, [filteredRows, page, rowsPerPage]); - const handleSearch = (inputs: Record) => { - setFilters(inputs); - setPage(0); // Reset to first page when searching + const handleSearch = async (inputs: Record) => { + setLoading(true); + setError(null); + try { + const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; + const uniqueCodes = new Map(); + (data || []).forEach((truck) => { + const code = String(truck.truckLanceCode ?? "").trim(); + if (code && !uniqueCodes.has(code)) { + uniqueCodes.set(code, truck); + } + }); + setTruckData(Array.from(uniqueCodes.values())); + setFilters(inputs); + setPage(0); + } catch (err: any) { + console.error("Failed to load truck lanes:", err); + setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); + } finally { + setLoading(false); + } }; const handlePageChange = (event: unknown, newPage: number) => { @@ -233,24 +225,6 @@ const TruckLane: React.FC = () => { } }; - if (loading) { - return ( - - - - ); - } - - if (error) { - return ( - - - {error} - - - ); - } - const criteria: Criterion[] = [ { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, { type: "time", label: t("Departure Time"), paramName: "departureTime" }, @@ -265,6 +239,7 @@ const TruckLane: React.FC = () => { criteria={criteria as Criterion[]} onSearch={handleSearch} onReset={() => { + setTruckData([]); setFilters({}); }} /> @@ -284,7 +259,17 @@ const TruckLane: React.FC = () => { {t("Add Truck Lane")}
    - + {error && ( + + {error} + + )} + + {loading ? ( + + + + ) : ( @@ -356,6 +341,7 @@ const TruckLane: React.FC = () => { rowsPerPageOptions={[5, 10, 25, 50]} /> + )} diff --git a/src/components/StockIssue/SearchPage.tsx b/src/components/StockIssue/SearchPage.tsx index 608dc31..dad5252 100644 --- a/src/components/StockIssue/SearchPage.tsx +++ b/src/components/StockIssue/SearchPage.tsx @@ -150,7 +150,7 @@ const SearchPage: React.FC = ({ dataList }) => { { name: "itemDescription", label: t("Item") }, { name: "lotNo", label: t("Lot No.") }, { name: "storeLocation", label: t("Location") }, - { name: "missQty", label: t("Miss Qty") }, + { name: "issueQty", label: t("Miss Qty") }, { name: "id", label: t("Action"), @@ -176,7 +176,7 @@ const SearchPage: React.FC = ({ dataList }) => { { name: "itemDescription", label: t("Item") }, { name: "lotNo", label: t("Lot No.") }, { name: "storeLocation", label: t("Location") }, - { name: "badItemQty", label: t("Defective Qty") }, + { name: "issueQty", label: t("Defective Qty") }, { name: "id", label: t("Action"), diff --git a/src/components/StockRecord/SearchPage.tsx b/src/components/StockRecord/SearchPage.tsx index 8d1c02a..025dd84 100644 --- a/src/components/StockRecord/SearchPage.tsx +++ b/src/components/StockRecord/SearchPage.tsx @@ -77,16 +77,10 @@ const SearchPage: React.FC = ({ dataList: initialDataList }) => { sorted.forEach((item) => { const currentBalance = balanceMap.get(item.itemId) || 0; - let newBalance = currentBalance; + - // 根据类型计算余额 - if (item.transactionType === "IN") { - newBalance = currentBalance + item.qty; - } else if (item.transactionType === "OUT") { - newBalance = currentBalance - item.qty; - } - - balanceMap.set(item.itemId, newBalance); + + // 格式化日期 - 优先使用 date 字段 let formattedDate = ""; @@ -128,7 +122,7 @@ const SearchPage: React.FC = ({ dataList: initialDataList }) => { formattedDate, inQty: item.transactionType === "IN" ? item.qty : 0, outQty: item.transactionType === "OUT" ? item.qty : 0, - balanceQty: item.balanceQty ? item.balanceQty : newBalance, + balanceQty: item.balanceQty ?? 0, }); }); diff --git a/src/components/StockTakeManagement/ApproverStockTake.tsx b/src/components/StockTakeManagement/ApproverStockTake.tsx index 512898a..0a263b5 100644 --- a/src/components/StockTakeManagement/ApproverStockTake.tsx +++ b/src/components/StockTakeManagement/ApproverStockTake.tsx @@ -404,6 +404,17 @@ const ApproverStockTake: React.FC = ({ ) : ( <> +
    diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx index e186194..9233ca8 100644 --- a/src/components/StockTakeManagement/PickerReStockTake.tsx +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -354,6 +354,17 @@ const PickerReStockTake: React.FC = ({ ) : ( <> +
    diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 9c49f44..cda39cf 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -443,6 +443,17 @@ const PickerStockTake: React.FC = ({ ) : ( <> +
    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/en/qcItemAll.json b/src/i18n/en/qcItemAll.json new file mode 100644 index 0000000..f587f48 --- /dev/null +++ b/src/i18n/en/qcItemAll.json @@ -0,0 +1,58 @@ +{ + "Qc Item All": "QC Management", + "Item and Qc Category Mapping": "Item and Qc Category Mapping", + "Qc Category and Qc Item Mapping": "Qc Category and Qc Item Mapping", + "Qc Category Management": "Qc Category Management", + "Qc Item Management": "Qc Item Management", + "Qc Category": "Qc Category", + "Qc Item": "Qc Item", + "Item": "Item", + "Code": "Code", + "Name": "Name", + "Description": "Description", + "Type": "Type", + "Order": "Order", + "Item Count": "Item Count", + "Qc Item Count": "Qc Item Count", + "Qc Category Count": "Qc Category Count", + "Actions": "Actions", + "View": "View", + "Edit": "Edit", + "Delete": "Delete", + "Add": "Add", + "Add Mapping": "Add Mapping", + "Add Association": "Add Association", + "Save": "Save", + "Cancel": "Cancel", + "Submit": "Submit", + "Details": "Details", + "Create Qc Category": "Create Qc Category", + "Edit Qc Category": "Edit Qc Category", + "Create Qc Item": "Create Qc Item", + "Edit Qc Item": "Edit Qc Item", + "Delete Success": "Delete Success", + "Delete Error": "Delete Error", + "Submit Success": "Submit Success", + "Submit Error": "Submit Error", + "Cannot Delete": "Cannot Delete", + "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.", + "Cannot delete QcItem. It is linked to one or more QcCategories.": "Cannot delete QcItem. It is linked to one or more QcCategories.", + "Select Item": "Select Item", + "Select Qc Category": "Select Qc Category", + "Select Qc Item": "Select Qc Item", + "Select Type": "Select Type", + "Item Code": "Item Code", + "Item Name": "Item Name", + "Qc Category Code": "Qc Category Code", + "Qc Category Name": "Qc Category Name", + "Qc Item Code": "Qc Item Code", + "Qc Item Name": "Qc Item Name", + "Mapping Details": "Mapping Details", + "Association Details": "Association Details", + "No mappings found": "No mappings found", + "No associations found": "No associations found", + "No data available": "No data available", + "Confirm Delete": "Confirm Delete", + "Are you sure you want to delete this item?": "Are you sure you want to delete this item?" +} + diff --git a/src/i18n/zh/dashboard.json b/src/i18n/zh/dashboard.json index 6fdfaec..c2cf9d7 100644 --- a/src/i18n/zh/dashboard.json +++ b/src/i18n/zh/dashboard.json @@ -65,7 +65,7 @@ "Last updated": "最後更新", "Truck Schedule": "車輛班次", "Time Remaining": "剩餘時間", - "No. of Shops": "門店數量", + "No. of Shops": "門店數量[提票數量]", "Total Items": "總貨品數", "Tickets Released": "已發放成品出倉單", "First Ticket Start": "首單開始時間", @@ -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/inventory.json b/src/i18n/zh/inventory.json index 831d0e0..d2e9663 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -33,6 +33,11 @@ "Start Time": "開始時間", "Difference": "差異", "stockTaking": "盤點中", + "rejected": "已拒絕", + "miss": "缺貨", + "bad": "不良", + "expiry": "過期", + "Bom Req. Qty": "需求數(BOM單位)", "selected stock take qty": "已選擇盤點數量", "book qty": "帳面庫存", "start time": "開始時間", diff --git a/src/i18n/zh/items.json b/src/i18n/zh/items.json index c2f200a..288c7b8 100644 --- a/src/i18n/zh/items.json +++ b/src/i18n/zh/items.json @@ -36,12 +36,19 @@ "LocationCode": "預設位置", "DefaultLocationCode": "預設位置", "Special Type": "特殊類型", -"None": "無", +"None": "正常", "isEgg": "雞蛋", "isFee": "費用", "isBag": "袋子", "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 diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index a66718c..21d94b0 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -203,6 +203,7 @@ "No Group": "沒有組", "No created items": "沒有創建物料", "Order Quantity": "需求數", + "Bom Req. Qty": "需求數(BOM單位)", "Selected": "已選擇", "Are you sure you want to delete this procoess?": "您確定要刪除此工序嗎?", "Please select item": "請選擇物料", diff --git a/src/i18n/zh/qcItemAll.json b/src/i18n/zh/qcItemAll.json new file mode 100644 index 0000000..370113b --- /dev/null +++ b/src/i18n/zh/qcItemAll.json @@ -0,0 +1,58 @@ +{ + "Qc Item All": "QC 綜合管理", + "Item and Qc Category Mapping": "物料與品檢模板映射", + "Qc Category and Qc Item Mapping": "品檢模板與品檢項目映射", + "Qc Category Management": "品檢模板管理", + "Qc Item Management": "品檢項目管理", + "Qc Category": "品檢模板", + "Qc Item": "品檢項目", + "Item": "物料", + "Code": "編號", + "Name": "名稱", + "Description": "描述", + "Type": "類型", + "Order": "順序", + "Item Count": "關聯物料數量", + "Qc Item Count": "關聯品檢項目數量", + "Qc Category Count": "關聯品檢模板數量", + "Actions": "操作", + "View": "查看", + "Edit": "編輯", + "Delete": "刪除", + "Add": "新增", + "Add Mapping": "新增映射", + "Add Association": "新增關聯", + "Save": "儲存", + "Cancel": "取消", + "Submit": "提交", + "Details": "詳情", + "Create Qc Category": "新增品檢模板", + "Edit Qc Category": "編輯品檢模板", + "Create Qc Item": "新增品檢項目", + "Edit Qc Item": "編輯品檢項目", + "Delete Success": "刪除成功", + "Delete Error": "刪除失敗", + "Submit Success": "提交成功", + "Submit Error": "提交失敗", + "Cannot Delete": "無法刪除", + "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "無法刪除品檢模板。它有 {itemCount} 個物料和 {qcItemCount} 個品檢項目與其關聯。", + "Cannot delete QcItem. It is linked to one or more QcCategories.": "無法刪除品檢項目。它與一個或多個品檢模板關聯。", + "Select Item": "選擇物料", + "Select Qc Category": "選擇品檢模板", + "Select Qc Item": "選擇品檢項目", + "Select Type": "選擇類型", + "Item Code": "物料編號", + "Item Name": "物料名稱", + "Qc Category Code": "品檢模板編號", + "Qc Category Name": "品檢模板名稱", + "Qc Item Code": "品檢項目編號", + "Qc Item Name": "品檢項目名稱", + "Mapping Details": "映射詳情", + "Association Details": "關聯詳情", + "No mappings found": "未找到映射", + "No associations found": "未找到關聯", + "No data available": "暫無數據", + "Confirm Delete": "確認刪除", + "Are you sure you want to delete this item?": "您確定要刪除此項目嗎?" +} +