Ver a proveniência

Merge remote-tracking branch 'origin/MergeProblem1' into MergeProblem1

MergeProblem1
B.E.N.S.O.N há 2 semanas
ascendente
cometimento
e62830e1e2
53 ficheiros alterados com 2882 adições e 211 eliminações
  1. +2
    -2
      src/app/(main)/ps/page.tsx
  2. +19
    -0
      src/app/(main)/settings/qcItem copy/create/not-found.tsx
  3. +26
    -0
      src/app/(main)/settings/qcItem copy/create/page.tsx
  4. +19
    -0
      src/app/(main)/settings/qcItem copy/edit/not-found.tsx
  5. +53
    -0
      src/app/(main)/settings/qcItem copy/edit/page.tsx
  6. +48
    -0
      src/app/(main)/settings/qcItem copy/page.tsx
  7. +47
    -0
      src/app/(main)/settings/qcItemAll/page.tsx
  8. +191
    -21
      src/app/(main)/testing/page.tsx
  9. +24
    -1
      src/app/api/bag/action.ts
  10. +5
    -2
      src/app/api/do/actions.tsx
  11. +2
    -2
      src/app/api/do/client.ts
  12. +30
    -0
      src/app/api/inventory/actions.ts
  13. +1
    -0
      src/app/api/settings/item/actions.ts
  14. +1
    -0
      src/app/api/settings/item/index.ts
  15. +54
    -2
      src/app/api/settings/m18ImportTesting/actions.ts
  16. +28
    -0
      src/app/api/settings/qcCategory/client.ts
  17. +9
    -0
      src/app/api/settings/qcCategory/index.ts
  18. +265
    -0
      src/app/api/settings/qcItemAll/actions.ts
  19. +101
    -0
      src/app/api/settings/qcItemAll/index.ts
  20. +4
    -3
      src/app/api/stockIssue/actions.ts
  21. +21
    -0
      src/app/api/warehouse/client.ts
  22. +11
    -0
      src/components/CreateItem/CreateItem.tsx
  23. +1
    -0
      src/components/CreateItem/CreateItemWrapper.tsx
  24. +57
    -2
      src/components/CreateItem/ProductDetails.tsx
  25. +200
    -0
      src/components/CreateItem/QcItemsList.tsx
  26. +40
    -4
      src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx
  27. +85
    -73
      src/components/InventorySearch/InventoryLotLineTable.tsx
  28. +9
    -13
      src/components/ItemsSearch/ItemsSearch.tsx
  29. +4
    -4
      src/components/M18ImportTesting/M18ImportDo.tsx
  30. +4
    -4
      src/components/M18ImportTesting/M18ImportPo.tsx
  31. +74
    -1
      src/components/M18ImportTesting/M18ImportTesting.tsx
  32. +9
    -4
      src/components/NavigationContent/NavigationContent.tsx
  33. +1
    -1
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  34. +105
    -0
      src/components/QcItemAll/QcItemAllTabs.tsx
  35. +351
    -0
      src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx
  36. +304
    -0
      src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx
  37. +226
    -0
      src/components/QcItemAll/Tab2QcCategoryManagement.tsx
  38. +226
    -0
      src/components/QcItemAll/Tab3QcItemManagement.tsx
  39. +0
    -6
      src/components/Shop/Shop.tsx
  40. +36
    -50
      src/components/Shop/TruckLane.tsx
  41. +2
    -2
      src/components/StockIssue/SearchPage.tsx
  42. +4
    -10
      src/components/StockRecord/SearchPage.tsx
  43. +11
    -0
      src/components/StockTakeManagement/ApproverStockTake.tsx
  44. +11
    -0
      src/components/StockTakeManagement/PickerReStockTake.tsx
  45. +11
    -0
      src/components/StockTakeManagement/PickerStockTake.tsx
  46. +5
    -0
      src/i18n/en/dashboard.json
  47. +8
    -1
      src/i18n/en/items.json
  48. +58
    -0
      src/i18n/en/qcItemAll.json
  49. +6
    -1
      src/i18n/zh/dashboard.json
  50. +5
    -0
      src/i18n/zh/inventory.json
  51. +9
    -2
      src/i18n/zh/items.json
  52. +1
    -0
      src/i18n/zh/jo.json
  53. +58
    -0
      src/i18n/zh/qcItemAll.json

+ 2
- 2
src/app/(main)/ps/page.tsx Ver ficheiro

@@ -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}` }
});


+ 19
- 0
src/app/(main)/settings/qcItem copy/create/not-found.tsx Ver ficheiro

@@ -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 (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">
{t("The create qc item page was not found!")}
</Typography>
<Link href="/qcItems" component={NextLink} variant="body2">
{t("Return to all qc items")}
</Link>
</Stack>
);
}

+ 26
- 0
src/app/(main)/settings/qcItem copy/create/page.tsx Ver ficheiro

@@ -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 (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Create Qc Item")}
</Typography>
<I18nProvider namespaces={["qcItem"]}>
<QcItemSave />
</I18nProvider>
</>
);
};

export default qcItem;

+ 19
- 0
src/app/(main)/settings/qcItem copy/edit/not-found.tsx Ver ficheiro

@@ -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 (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">
{t("The edit qc item page was not found!")}
</Typography>
<Link href="/settings/qcItems" component={NextLink} variant="body2">
{t("Return to all qc items")}
</Link>
</Stack>
);
}

+ 53
- 0
src/app/(main)/settings/qcItem copy/edit/page.tsx Ver ficheiro

@@ -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<Props> = 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 (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Edit Qc Item")}
</Typography>
<I18nProvider namespaces={["qcItem"]}>
<QcItemSave id={id} />
</I18nProvider>
</>
);
};

export default qcItem;

+ 48
- 0
src/app/(main)/settings/qcItem copy/page.tsx Ver ficheiro

@@ -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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Qc Item")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="qcItem/create"
>
{t("Create Qc Item")}
</Button>
</Stack>
<Suspense fallback={<QcItemSearch.Loading />}>
<I18nProvider namespaces={["common", "qcItem"]}>
<QcItemSearch />
</I18nProvider>
</Suspense>
</>
);
};

export default qcItem;

+ 47
- 0
src/app/(main)/settings/qcItemAll/page.tsx Ver ficheiro

@@ -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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
sx={{ mb: 3 }}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Qc Item All")}
</Typography>
</Stack>
<Suspense fallback={<div>Loading...</div>}>
<I18nProvider namespaces={["common", "qcItemAll", "qcCategory", "qcItem"]}>
<QcItemAllTabs
tab0Content={<Tab0ItemQcCategoryMapping />}
tab1Content={<Tab1QcCategoryQcItemMapping />}
tab2Content={<Tab2QcCategoryManagement />}
tab3Content={<Tab3QcItemManagement />}
/>
</I18nProvider>
</Suspense>
</>
);
};

export default qcItemAll;


+ 191
- 21
src/app/(main)/testing/page.tsx Ver ficheiro

@@ -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 (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
</div>
);
}

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 }) => (
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, minHeight: '450px', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" gutterBottom color="primary" sx={{ borderBottom: '2px solid #f0f0f0', pb: 1, mb: 2 }}>
{title}
</Typography>
{children || <Typography color="textSecondary" sx={{ m: 'auto' }}>Waiting for implementation...</Typography>}
</Paper>
</Grid>
<Paper sx={{ p: 3, minHeight: '450px', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" gutterBottom color="primary" sx={{ borderBottom: '2px solid #f0f0f0', pb: 1, mb: 2 }}>
{title}
</Typography>
{children || <Typography color="textSecondary" sx={{ m: 'auto' }}>Waiting for implementation...</Typography>}
</Paper>
);

return (
<Box sx={{ p: 4 }}>
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Printer Testing Dashboard</Typography>
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Printer Testing</Typography>
<Grid container spacing={3}>
{/* 1. TSC Section */}
<Tabs value={tabValue} onChange={handleTabChange} aria-label="printer sections tabs" centered variant="fullWidth">
<Tab label="1. TSC" />
<Tab label="2. DataFlex" />
<Tab label="3. OnPack" />
<Tab label="4. Laser" />
<Tab label="5. HANS600S-M" />
</Tabs>

<TabPanel value={tabValue} index={0}>
<Section title="1. TSC">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField size="small" label="Printer IP" value={tscConfig.ip} onChange={e => setTscConfig({...tscConfig, ip: e.target.value})} />
@@ -181,8 +261,9 @@ const [laserItems, setLaserItems] = useState([
</Table>
</TableContainer>
</Section>
</TabPanel>

{/* 2. DataFlex Section */}
<TabPanel value={tabValue} index={1}>
<Section title="2. DataFlex">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField size="small" label="Printer IP" value={dfConfig.ip} onChange={e => setDfConfig({...dfConfig, ip: e.target.value})} />
@@ -214,8 +295,9 @@ const [laserItems, setLaserItems] = useState([
</Table>
</TableContainer>
</Section>
</TabPanel>

{/* 3. OnPack Section */}
<TabPanel value={tabValue} index={2}>
<Section title="3. OnPack">
<Box sx={{ m: 'auto', textAlign: 'center' }}>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
@@ -226,8 +308,9 @@ const [laserItems, setLaserItems] = useState([
</Button>
</Box>
</Section>
</TabPanel>

{/* 4. Laser Section (HANS600S-M) */}
<TabPanel value={tabValue} index={3}>
<Section title="4. Laser">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField size="small" label="Laser IP" value={laserConfig.ip} onChange={e => setLaserConfig({...laserConfig, ip: e.target.value})} />
@@ -283,7 +366,94 @@ const [laserItems, setLaserItems] = useState([
Note: HANS Laser requires pre-saved templates on the controller.
</Typography>
</Section>
</Grid>
</TabPanel>

<TabPanel value={tabValue} index={4}>
<Section title="5. HANS600S-M">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField
size="small"
label="Laser IP"
value={hansConfig.ip}
onChange={e => setHansConfig({...hansConfig, ip: e.target.value})}
/>
<TextField
size="small"
label="Port"
value={hansConfig.port}
onChange={e => setHansConfig({...hansConfig, port: e.target.value})}
/>
<Router color="action" sx={{ ml: 'auto' }} />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Ch3 Text (SN)</TableCell>
<TableCell>Ch4 Text (Batch)</TableCell>
<TableCell>Obj3 Name</TableCell>
<TableCell>Obj4 Name</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{hansItems.map(row => (
<TableRow key={row.id}>
<TableCell>
<TextField
variant="standard"
value={row.textChannel3}
onChange={e => handleItemChange(setHansItems, row.id, 'textChannel3', e.target.value)}
sx={{ minWidth: 180 }}
/>
</TableCell>
<TableCell>
<TextField
variant="standard"
value={row.textChannel4}
onChange={e => handleItemChange(setHansItems, row.id, 'textChannel4', e.target.value)}
sx={{ minWidth: 140 }}
/>
</TableCell>
<TableCell>
<TextField
variant="standard"
value={row.text3ObjectName}
onChange={e => handleItemChange(setHansItems, row.id, 'text3ObjectName', e.target.value)}
size="small"
/>
</TableCell>
<TableCell>
<TextField
variant="standard"
value={row.text4ObjectName}
onChange={e => handleItemChange(setHansItems, row.id, 'text4ObjectName', e.target.value)}
size="small"
/>
</TableCell>
<TableCell align="center">
<Button
variant="contained"
color="error"
size="small"
startIcon={<Print />}
onClick={() => handleHansPrint(row)}
sx={{ minWidth: 80 }}
>
TCP Mark
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Typography variant="caption" sx={{ mt: 2, display: 'block', color: 'text.secondary', fontSize: '0.75rem' }}>
TCP Push to EZCAD3 (Ch3/Ch4 via E3_SetTextObject) | IP:192.168.76.10:45678 | Backend: /print-laser-tcp
</Typography>
</Section>
</TabPanel>

{/* Dialog for OnPack */}
<Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm">


+ 24
- 1
src/app/api/bag/action.ts Ver ficheiro

@@ -118,4 +118,27 @@ export const fetchBagLotLines = cache(async (bagId: number) =>

export const fetchBagConsumptions = cache(async (bagLotLineId: number) =>
serverFetchJson<BagConsumptionResponse[]>(`${BASE_API_URL}/bag/lot-lines/${bagLotLineId}/consumptions`, { method: "GET" })
);
);

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<SoftDeleteBagResponse> => {
const response = await serverFetchJson<SoftDeleteBagResponse>(
`${BASE_API_URL}/bag/by-item/${itemId}/soft-delete`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
}
);
revalidateTag("bagInfo");
revalidateTag("bags");
return response;
};

+ 5
- 2
src/app/api/do/actions.tsx Ver ficheiro

@@ -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<TruckScheduleDashboardItem[]>(
`${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`,
url,
{
method: "GET",
}


+ 2
- 2
src/app/api/do/client.ts Ver ficheiro

@@ -5,8 +5,8 @@ import {
type TruckScheduleDashboardItem
} from "./actions";

export const fetchTruckScheduleDashboardClient = async (): Promise<TruckScheduleDashboardItem[]> => {
return await fetchTruckScheduleDashboard();
export const fetchTruckScheduleDashboardClient = async (date?: string): Promise<TruckScheduleDashboardItem[]> => {
return await fetchTruckScheduleDashboard(date);
};

export type { TruckScheduleDashboardItem };


+ 30
- 0
src/app/api/inventory/actions.ts Ver ficheiro

@@ -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<MessageResponse>(
`${BASE_API_URL}/stockTransferRecord/create`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("inventoryLotLines");
revalidateTag("inventories");
return result;
};

+ 1
- 0
src/app/api/settings/item/actions.ts Ver ficheiro

@@ -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) => {


+ 1
- 0
src/app/api/settings/item/index.ts Ver ficheiro

@@ -67,6 +67,7 @@ export type ItemsResult = {
export type Result = {
item: ItemsResult;
qcChecks: ItemQc[];
qcType?: string;
};
export const fetchAllItems = cache(async () => {
return serverFetchJson<ItemsResult[]>(`${BASE_API_URL}/items`, {


+ 54
- 2
src/app/api/settings/m18ImportTesting/actions.ts Ver ficheiro

@@ -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.";
}
};

+ 28
- 0
src/app/api/settings/qcCategory/client.ts Ver ficheiro

@@ -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<QcItemInfo[]> => {
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();
};




+ 9
- 0
src/app/api/settings/qcCategory/index.ts Ver ficheiro

@@ -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();
};


+ 265
- 0
src/app/api/settings/qcItemAll/actions.ts Ver ficheiro

@@ -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<string, string> | 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<string, string> | null;
}

// Item and QcCategory mapping
export const getItemQcCategoryMappings = async (
qcCategoryId?: number,
itemId?: number
): Promise<ItemQcCategoryMappingInfo[]> => {
const params = new URLSearchParams();
if (qcCategoryId) params.append("qcCategoryId", qcCategoryId.toString());
if (itemId) params.append("itemId", itemId.toString());
return serverFetchJson<ItemQcCategoryMappingInfo[]>(
`${BASE_API_URL}/qcItemAll/itemMappings?${params.toString()}`
);
};

export const saveItemQcCategoryMapping = async (
itemId: number,
qcCategoryId: number,
type: string
): Promise<ItemQcCategoryMappingInfo> => {
const params = new URLSearchParams();
params.append("itemId", itemId.toString());
params.append("qcCategoryId", qcCategoryId.toString());
params.append("type", type);
const response = await serverFetchJson<ItemQcCategoryMappingInfo>(
`${BASE_API_URL}/qcItemAll/itemMapping?${params.toString()}`,
{
method: "POST",
}
);
revalidateTag("qcItemAll");
return response;
};

export const deleteItemQcCategoryMapping = async (
mappingId: number
): Promise<void> => {
await serverFetchJson<void>(
`${BASE_API_URL}/qcItemAll/itemMapping/${mappingId}`,
{
method: "DELETE",
}
);
revalidateTag("qcItemAll");
};

// QcCategory and QcItem mapping
export const getQcCategoryQcItemMappings = async (
qcCategoryId: number
): Promise<QcItemInfo[]> => {
return serverFetchJson<QcItemInfo[]>(
`${BASE_API_URL}/qcItemAll/qcItemMappings/${qcCategoryId}`
);
};

export const saveQcCategoryQcItemMapping = async (
qcCategoryId: number,
qcItemId: number,
order: number,
description?: string
): Promise<QcItemInfo> => {
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<QcItemInfo>(
`${BASE_API_URL}/qcItemAll/qcItemMapping?${params.toString()}`,
{
method: "POST",
}
);
revalidateTag("qcItemAll");
return response;
};

export const deleteQcCategoryQcItemMapping = async (
mappingId: number
): Promise<void> => {
await serverFetchJson<void>(
`${BASE_API_URL}/qcItemAll/qcItemMapping/${mappingId}`,
{
method: "DELETE",
}
);
revalidateTag("qcItemAll");
};

// Counts
export const getItemCountByQcCategory = async (
qcCategoryId: number
): Promise<number> => {
return serverFetchJson<number>(
`${BASE_API_URL}/qcItemAll/itemCount/${qcCategoryId}`
);
};

export const getQcItemCountByQcCategory = async (
qcCategoryId: number
): Promise<number> => {
return serverFetchJson<number>(
`${BASE_API_URL}/qcItemAll/qcItemCount/${qcCategoryId}`
);
};

// Validation
export const canDeleteQcCategory = async (id: number): Promise<boolean> => {
return serverFetchJson<boolean>(
`${BASE_API_URL}/qcItemAll/canDeleteQcCategory/${id}`
);
};

export const canDeleteQcItem = async (id: number): Promise<boolean> => {
return serverFetchJson<boolean>(
`${BASE_API_URL}/qcItemAll/canDeleteQcItem/${id}`
);
};

// Save and delete with validation
export const saveQcCategoryWithValidation = async (
data: SaveQcCategoryInputs
): Promise<SaveQcCategoryResponse> => {
const response = await serverFetchJson<SaveQcCategoryResponse>(
`${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<DeleteResponse> => {
const response = await serverFetchJson<DeleteResponse>(
`${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<SaveQcItemResponse> => {
const response = await serverFetchJson<SaveQcItemResponse>(
`${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<DeleteResponse> => {
const response = await serverFetchJson<DeleteResponse>(
`${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<QcCategoryResult[]> => {
return serverFetchJson<QcCategoryResult[]>(`${BASE_API_URL}/qcCategories`, {
next: { tags: ["qcCategories"] },
});
};

export const fetchItemsForAll = async (): Promise<ItemsResult[]> => {
return serverFetchJson<ItemsResult[]>(`${BASE_API_URL}/items`, {
next: { tags: ["items"] },
});
};

export const fetchQcItemsForAll = async (): Promise<QcItemResult[]> => {
return serverFetchJson<QcItemResult[]>(`${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<ItemsResult | null> => {
try {
return await serverFetchJson<ItemsResult>(`${BASE_API_URL}/qcItemAll/itemByCode/${encodeURIComponent(code)}`);
} catch (error) {
// Item not found
return null;
}
};




+ 101
- 0
src/app/api/settings/qcItemAll/index.ts Ver ficheiro

@@ -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;
}


+ 4
- 3
src/app/api/stockIssue/actions.ts Ver ficheiro

@@ -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;


+ 21
- 0
src/app/api/warehouse/client.ts Ver ficheiro

@@ -31,4 +31,25 @@ export const exportWarehouseQrCode = async (warehouseIds: number[]): Promise<{ b

return { blobValue, filename };
};

export const fetchWarehouseListClient = async (): Promise<WarehouseResult[]> => {
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

+ 11
- 0
src/components/CreateItem/CreateItem.tsx Ver ficheiro

@@ -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<Props> = ({
);
} 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);
}
}


+ 1
- 0
src/components/CreateItem/CreateItemWrapper.tsx Ver ficheiro

@@ -51,6 +51,7 @@ const CreateItemWrapper: React.FC<Props> & 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,


+ 57
- 2
src/components/CreateItem/ProductDetails.tsx Ver ficheiro

@@ -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<Props> = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => {
const [qcItems, setQcItems] = useState<QcItemInfo[]>([]);
const [qcItemsLoading, setQcItemsLoading] = useState(false);

const {
t,
i18n: { language },
} = useTranslation();
} = useTranslation("items");

const {
register,
@@ -121,6 +125,30 @@ const ProductDetails: React.FC<Props> = ({ 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 (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
@@ -216,6 +244,26 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehous
)}
/>
</Grid>
<Grid item xs={6}>
<Controller
control={control}
name="qcType"
render={({ field }) => (
<FormControl fullWidth>
<InputLabel>{t("QC Type")}</InputLabel>
<Select
value={field.value || ""}
label={t("QC Type")}
onChange={field.onChange}
onBlur={field.onBlur}
>
<MenuItem value="IPQC">{t("IPQC")}</MenuItem>
<MenuItem value="EPQC">{t("EPQC")}</MenuItem>
</Select>
</FormControl>
)}
/>
</Grid>
<Grid item xs={6}>
<Controller
control={control}
@@ -292,6 +340,13 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehous
</RadioGroup>
</FormControl>
</Grid>
<Grid item xs={12}>
<QcItemsList
qcItems={qcItems}
loading={qcItemsLoading}
categorySelected={!!qcCategoryId}
/>
</Grid>
<Grid item xs={12}>
<Stack
direction="row"


+ 200
- 0
src/components/CreateItem/QcItemsList.tsx Ver ficheiro

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

import { QcItemInfo } from "@/app/api/settings/qcCategory";
import {
Box,
Card,
CircularProgress,
Divider,
List,
ListItem,
Stack,
Typography,
} from "@mui/material";
import { CheckCircleOutline, FormatListNumbered } from "@mui/icons-material";
import { useTranslation } from "react-i18next";

type Props = {
qcItems: QcItemInfo[];
loading?: boolean;
categorySelected?: boolean;
};

const QcItemsList: React.FC<Props> = ({
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 (
<Box
display="flex"
justifyContent="center"
alignItems="center"
py={4}
sx={{
backgroundColor: "grey.50",
borderRadius: 2,
border: "1px dashed",
borderColor: "grey.300",
}}
>
<CircularProgress size={24} sx={{ mr: 1.5 }} />
<Typography variant="body2" color="text.secondary">
{t("Loading QC items...")}
</Typography>
</Box>
);
}

if (!categorySelected) {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
py={4}
sx={{
backgroundColor: "grey.50",
borderRadius: 2,
border: "1px dashed",
borderColor: "grey.300",
}}
>
<FormatListNumbered
sx={{ fontSize: 40, color: "grey.400", mb: 1 }}
/>
<Typography variant="body2" color="text.secondary">
{t("Select a QC template to view items")}
</Typography>
</Box>
);
}

if (sortedItems.length === 0) {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
py={4}
sx={{
backgroundColor: "grey.50",
borderRadius: 2,
border: "1px dashed",
borderColor: "grey.300",
}}
>
<CheckCircleOutline
sx={{ fontSize: 40, color: "grey.400", mb: 1 }}
/>
<Typography variant="body2" color="text.secondary">
{t("No QC items in this template")}
</Typography>
</Box>
);
}

return (
<Card
variant="outlined"
sx={{
borderRadius: 2,
backgroundColor: "background.paper",
overflow: "hidden",
}}
>
<Box
sx={{
px: 2,
py: 1.5,
backgroundColor: "primary.main",
color: "primary.contrastText",
}}
>
<Stack direction="row" alignItems="center" spacing={1}>
<FormatListNumbered fontSize="small" />
<Typography variant="subtitle2" fontWeight={600}>
{t("QC Checklist")} ({sortedItems.length})
</Typography>
</Stack>
</Box>
<List disablePadding>
{sortedItems.map((item, index) => (
<Box key={item.id}>
{index > 0 && <Divider />}
<ListItem
sx={{
py: 1.5,
px: 2,
"&:hover": {
backgroundColor: "action.hover",
},
}}
>
<Stack
direction="row"
spacing={2}
alignItems="flex-start"
width="100%"
>
{/* Order Number */}
<Typography
variant="body1"
fontWeight={600}
color="text.secondary"
sx={{ minWidth: 24 }}
>
{item.order}.
</Typography>
{/* Content */}
<Stack
direction="row"
alignItems="center"
spacing={2}
flex={1}
minWidth={0}
>
<Typography
variant="body1"
fontWeight={500}
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flexShrink: 0,
}}
>
{item.name || item.code}
</Typography>
{item.description && (
<Typography
variant="body2"
color="text.secondary"
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{item.description}
</Typography>
)}
</Stack>
</Stack>
</ListItem>
</Box>
))}
</List>
</Card>
);
};

export default QcItemsList;


+ 40
- 4
src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx Ver ficheiro

@@ -35,6 +35,7 @@ interface CompletedTracker {
const TruckScheduleDashboard: React.FC = () => {
const { t } = useTranslation("dashboard");
const [selectedStore, setSelectedStore] = useState<string>("");
const [selectedDate, setSelectedDate] = useState<string>("today");
const [data, setData] = useState<TruckScheduleDashboardItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// Initialize as null to avoid SSR/client hydration mismatch
@@ -43,6 +44,23 @@ const TruckScheduleDashboard: React.FC = () => {
const completedTrackerRef = useRef<Map<string, CompletedTracker>>(new Map());
const refreshCountRef = useRef<number>(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 = () => {
<MenuItem value="4/F">4/F</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel id="date-select-label" shrink={true}>
{t("Select Date")}
</InputLabel>
<Select
labelId="date-select-label"
id="date-select"
value={selectedDate}
label={t("Select Date")}
onChange={(e) => setSelectedDate(e.target.value)}
>
<MenuItem value="today">{t("Today")} ({getDateLabel(0)})</MenuItem>
<MenuItem value="tomorrow">{t("Tomorrow")} ({getDateLabel(1)})</MenuItem>
<MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem>
</Select>
</FormControl>
<Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}>
{t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'}
@@ -290,7 +326,7 @@ const TruckScheduleDashboard: React.FC = () => {
<TableRow>
<TableCell colSpan={10} align="center">
<Typography variant="body2" color="text.secondary">
{t("No truck schedules available for today")}
{t("No truck schedules available")} ({getDateParam(selectedDate)})
</Typography>
</TableCell>
</TableRow>


+ 85
- 73
src/components/InventorySearch/InventoryLotLineTable.tsx Ver ficheiro

@@ -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<Props> = ({ inventoryLotLines, pagingContr
const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false);
const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null);
const [startLocation, setStartLocation] = useState<string>("");
const [targetLocation, setTargetLocation] = useState<string>("");
const [targetLocation, setTargetLocation] = useState<number | null>(null); // Store warehouse ID instead of code
const [targetLocationInput, setTargetLocationInput] = useState<string>("");
const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0);
const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]);
@@ -65,7 +65,7 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
setSelectedLotLine(lotLine);
setStockTransferModalOpen(true);
setStartLocation(lotLine.warehouse.code || "");
setTargetLocation("");
setTargetLocation(null);
setTargetLocationInput("");
setQtyToBeTransferred(0);
},
@@ -188,34 +188,46 @@ const InventoryLotLineTable: React.FC<Props> = ({ 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 <>
<Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography>
@@ -276,55 +288,55 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr
</Grid>
<Grid item xs={5.5}>
<Autocomplete
options={warehouses.filter(w => 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) => (
<li {...props}>
{option.code}
</li>
)}
renderInput={(params) => (
<TextField
{...params}
label={t("Target Location")}
variant="outlined"
fullWidth
InputLabelProps={{
shrink: !!targetLocation || !!targetLocationInput,
sx: { fontSize: "0.9375rem" },
}}
isOptionEqualToValue={(option, value) => option.code === value.code}
autoHighlight={false}
autoSelect={false}
clearOnBlur={false}
renderOption={(props, option) => (
<li {...props}>
{option.code}
</li>
)}
renderInput={(params) => (
<TextField
{...params}
label={t("Target Location")}
variant="outlined"
fullWidth
InputLabelProps={{
shrink: !!targetLocation || !!targetLocationInput,
sx: { fontSize: "0.9375rem" },
}}
/>
)}
/>
)}
/>
</Grid>
</Grid>


+ 9
- 13
src/components/ItemsSearch/ItemsSearch.tsx Ver ficheiro

@@ -124,12 +124,13 @@ const ItemsSearch: React.FC<Props> = ({ 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<Column<ItemsResultWithStatus>[]>(
@@ -181,25 +182,20 @@ const ItemsSearch: React.FC<Props> = ({ items }) => {
);

const onReset = useCallback(() => {
setFilteredItems(items);
}, [items]);
setFilteredItems([]);
setFilterObj({});
setTotalCount(0);
}, []);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// 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}
/>


+ 4
- 4
src/components/M18ImportTesting/M18ImportDo.tsx Ver ficheiro

@@ -70,7 +70,7 @@ const M18ImportDo: React.FC<Props> = ({}) => {
<Box display="flex">
<Controller
control={control}
name="do.modifiedDateFrom"
name="do.dDateFrom"
// rules={{
// required: "Please input the date From!",
// validate: {
@@ -80,7 +80,7 @@ const M18ImportDo: React.FC<Props> = ({}) => {
// }}
render={({ field, fieldState: { error } }) => (
<DateTimePicker
label={t("Modified Date From *")}
label={t("Delivery Date From *")}
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
onChange={(newValue: Dayjs | null) =>
handleDateTimePickerOnChange(newValue, field.onChange)
@@ -104,7 +104,7 @@ const M18ImportDo: React.FC<Props> = ({}) => {
</Box>
<Controller
control={control}
name="do.modifiedDateTo"
name="do.dDateTo"
// rules={{
// required: "Please input the date to!",
// validate: {
@@ -116,7 +116,7 @@ const M18ImportDo: React.FC<Props> = ({}) => {
// }}
render={({ field, fieldState: { error } }) => (
<DateTimePicker
label={t("Modified Date To *")}
label={t("Delivery Date To *")}
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
onChange={(newValue: Dayjs | null) =>
handleDateTimePickerOnChange(newValue, field.onChange)


+ 4
- 4
src/components/M18ImportTesting/M18ImportPo.tsx Ver ficheiro

@@ -70,7 +70,7 @@ const M18ImportPo: React.FC<Props> = ({}) => {
<Box display="flex">
<Controller
control={control}
name="po.modifiedDateFrom"
name="po.dDateFrom"
// rules={{
// required: "Please input the date From!",
// validate: {
@@ -80,7 +80,7 @@ const M18ImportPo: React.FC<Props> = ({}) => {
// }}
render={({ field, fieldState: { error } }) => (
<DateTimePicker
label={t("Modified Date From *")}
label={t("Delivery Date From *")}
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
onChange={(newValue: Dayjs | null) =>
handleDateTimePickerOnChange(newValue, field.onChange)
@@ -104,7 +104,7 @@ const M18ImportPo: React.FC<Props> = ({}) => {
</Box>
<Controller
control={control}
name="po.modifiedDateTo"
name="po.dDateTo"
// rules={{
// required: "Please input the date to!",
// validate: {
@@ -116,7 +116,7 @@ const M18ImportPo: React.FC<Props> = ({}) => {
// }}
render={({ field, fieldState: { error } }) => (
<DateTimePicker
label={t("Modified Date To *")}
label={t("Delivery Date To *")}
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
onChange={(newValue: Dayjs | null) =>
handleDateTimePickerOnChange(newValue, field.onChange)


+ 74
- 1
src/components/M18ImportTesting/M18ImportTesting.tsx Ver ficheiro

@@ -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<Props> = ({}) => {
// [],
// );

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 (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="h6">{t("Manual Scheduler Triggers")}</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
<Button
variant="outlined"
startIcon={<PlayArrow />}
onClick={() => handleManualTrigger('po')}
disabled={isLoading}
>
Trigger PO
</Button>
<Button
variant="outlined"
startIcon={<PlayArrow />}
onClick={() => handleManualTrigger('do1')}
disabled={isLoading}
>
Trigger DO1
</Button>
<Button
variant="outlined"
startIcon={<PlayArrow />}
onClick={() => handleManualTrigger('do2')}
disabled={isLoading}
>
Trigger DO2
</Button>
<Button
variant="outlined"
startIcon={<PlayArrow />}
onClick={() => handleManualTrigger('master-data')}
disabled={isLoading}
>
Trigger Master
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<RefreshIcon />}
onClick={handleRefreshSchedules} // This now uses the logic that works
disabled={isLoading}
>
Reload Cron Settings
</Button>
</Stack>

<hr style={{ opacity: 0.2 }} />

<Typography variant="overline">
{t("Status: ")}
{isLoading ? t(`Processing ${loadingType}...`) : t("Ready")}
</Typography>

<Typography variant="overline">
{t("Status: ")}
{isLoading ? t(`Importing ${loadingType}...`) : t("Ready to import")}


+ 9
- 4
src/components/NavigationContent/NavigationContent.tsx Ver ficheiro

@@ -247,7 +247,7 @@ const NavigationContent: React.FC = () => {
icon: <BugReportIcon />,
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: <QrCodeIcon />,
label: "QR Code Handle",
path: "/settings/qrCodeHandle",
icon: <RequestQuote />,
label: "QC Check Template",
path: "/settings/user",
},
{
icon: <RequestQuote />,
label: "QC Item All",
path: "/settings/qcItemAll",
},
// {
// icon: <RequestQuote />,


+ 1
- 1
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx Ver ficheiro

@@ -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",


+ 105
- 0
src/components/QcItemAll/QcItemAllTabs.tsx Ver ficheiro

@@ -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 (
<div
role="tabpanel"
hidden={value !== index}
id={`qc-item-all-tabpanel-${index}`}
aria-labelledby={`qc-item-all-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
}

interface QcItemAllTabsProps {
tab0Content: ReactNode;
tab1Content: ReactNode;
tab2Content: ReactNode;
tab3Content: ReactNode;
}

const QcItemAllTabs: React.FC<QcItemAllTabsProps> = ({
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 (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={t("Item and Qc Category Mapping")} />
<Tab label={t("Qc Category and Qc Item Mapping")} />
<Tab label={t("Qc Category Management")} />
<Tab label={t("Qc Item Management")} />
</Tabs>
</Box>

<TabPanel value={currentTab} index={0}>
{tab0Content}
</TabPanel>

<TabPanel value={currentTab} index={1}>
{tab1Content}
</TabPanel>

<TabPanel value={currentTab} index={2}>
{tab2Content}
</TabPanel>

<TabPanel value={currentTab} index={3}>
{tab3Content}
</TabPanel>
</Box>
);
};

export default QcItemAllTabs;


+ 351
- 0
src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx Ver ficheiro

@@ -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<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab0ItemQcCategoryMapping: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null);
const [mappings, setMappings] = useState<ItemQcCategoryMappingInfo[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [itemCode, setItemCode] = useState<string>("");
const [validatedItem, setValidatedItem] = useState<ItemsResult | null>(null);
const [itemCodeError, setItemCodeError] = useState<string>("");
const [validatingItemCode, setValidatingItemCode] = useState<boolean>(false);
const [selectedType, setSelectedType] = useState<string>("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<SearchParamNames>[] = 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<Column<QcCategoryResult>[]>(
() => [
{ 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: <Edit />,
buttonIcons: {} as any,
sx: columnWidthSx("10%"),
},
],
[t, handleViewMappings]
);

if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "200px" }}>
<CircularProgress />
</Box>
);
}

return (
<Box>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcCategories(
qcCategories.filter(
(qc) =>
(!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcCategoryResult>
items={filteredQcCategories}
columns={columns}
/>

{/* View Mappings Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{t("Mapping Details")} - {selectedCategory?.name}
</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddMapping}
>
{t("Add Mapping")}
</Button>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Type")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{mappings.length === 0 ? (
<TableRow>
<TableCell colSpan={4} align="center">
{t("No mappings found")}
</TableCell>
</TableRow>
) : (
mappings.map((mapping) => (
<TableRow key={mapping.id}>
<TableCell>{mapping.itemCode}</TableCell>
<TableCell>{mapping.itemName}</TableCell>
<TableCell>{mapping.type}</TableCell>
<TableCell>
<IconButton
color="error"
size="small"
onClick={() => handleDeleteMapping(mapping.id)}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
</DialogActions>
</Dialog>

{/* Add Mapping Dialog */}
<Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>{t("Add Mapping")}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 2 }}>
<TextField
label={t("Item Code")}
value={itemCode}
onChange={(e) => 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 ? <CircularProgress size={20} /> : null,
}}
/>
<TextField
select
label={t("Select Type")}
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
SelectProps={{
native: true,
}}
fullWidth
>
{typeOptions.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</TextField>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
<Button
variant="contained"
onClick={handleSaveMapping}
disabled={!validatedItem}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};

export default Tab0ItemQcCategoryMapping;


+ 304
- 0
src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx Ver ficheiro

@@ -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<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab1QcCategoryQcItemMapping: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null);
const [mappings, setMappings] = useState<QcItemInfo[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [qcItems, setQcItems] = useState<QcItemResult[]>([]);
const [selectedQcItem, setSelectedQcItem] = useState<QcItemResult | null>(null);
const [order, setOrder] = useState<number>(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<SearchParamNames>[] = 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<Column<QcCategoryResult>[]>(
() => [
{ 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: <Edit />,
buttonIcons: {} as any,
sx: columnWidthSx("10%"),
},
],
[t, handleViewMappings]
);

return (
<Box>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcCategories(
qcCategories.filter(
(qc) =>
(!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcCategoryResult>
items={filteredQcCategories}
columns={columns}
/>

{/* View Mappings Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{t("Association Details")} - {selectedCategory?.name}
</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddMapping}
>
{t("Add Association")}
</Button>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Order")}</TableCell>
<TableCell>{t("Qc Item Code")}</TableCell>
<TableCell>{t("Qc Item Name")}</TableCell>
<TableCell>{t("Description")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{mappings.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
{t("No associations found")}
</TableCell>
</TableRow>
) : (
mappings.map((mapping) => (
<TableRow key={mapping.id}>
<TableCell>{mapping.order}</TableCell>
<TableCell>{mapping.code}</TableCell>
<TableCell>{mapping.name}</TableCell>
<TableCell>{mapping.description || "-"}</TableCell>
<TableCell>
<IconButton
color="error"
size="small"
onClick={() => handleDeleteMapping(mapping.id)}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
</DialogActions>
</Dialog>

{/* Add Mapping Dialog */}
<Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>{t("Add Association")}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 2 }}>
<Autocomplete
options={qcItems}
getOptionLabel={(option) => `${option.code} - ${option.name}`}
value={selectedQcItem}
onChange={(_, newValue) => setSelectedQcItem(newValue)}
renderInput={(params) => (
<TextField {...params} label={t("Select Qc Item")} />
)}
/>
<TextField
type="number"
label={t("Order")}
value={order}
onChange={(e) => setOrder(parseInt(e.target.value) || 0)}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
<Button
variant="contained"
onClick={handleSaveMapping}
disabled={!selectedQcItem}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};

export default Tab1QcCategoryQcItemMapping;


+ 226
- 0
src/components/QcItemAll/Tab2QcCategoryManagement.tsx Ver ficheiro

@@ -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<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab2QcCategoryManagement: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [editingCategory, setEditingCategory] = useState<QcCategoryResult | null>(null);

useEffect(() => {
loadCategories();
}, []);

const loadCategories = async () => {
const categories = await fetchQcCategoriesForAll();
setQcCategories(categories);
setFilteredQcCategories(categories);
};

const formProps = useForm<SaveQcCategoryInputs>({
defaultValues: {
code: "",
name: "",
description: "",
},
});

const searchCriteria: Criterion<SearchParamNames>[] = 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) + "<br>";
}
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<Column<QcCategoryResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: handleEdit,
buttonIcon: <EditNote />,
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: <Delete />,
buttonColor: "error",
sx: columnWidthSx("5%"),
},
],
[t, handleEdit, handleDelete]
);

return (
<>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAdd}
>
{t("Create Qc Category")}
</Button>
</Stack>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcCategories(
qcCategories.filter(
(qc) =>
(!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcCategoryResult>
items={filteredQcCategories}
columns={columns}
/>

{/* Add/Edit Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{editingCategory ? t("Edit Qc Category") : t("Create Qc Category")}
</DialogTitle>
<FormProvider {...formProps}>
<form onSubmit={formProps.handleSubmit(handleSubmit)}>
<DialogContent>
<QcCategoryDetails />
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
<Button type="submit" variant="contained">
{t("Submit")}
</Button>
</DialogActions>
</form>
</FormProvider>
</Dialog>
</>
);
};

export default Tab2QcCategoryManagement;


+ 226
- 0
src/components/QcItemAll/Tab3QcItemManagement.tsx Ver ficheiro

@@ -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<Omit<QcItemResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab3QcItemManagement: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcItems, setQcItems] = useState<QcItemResult[]>([]);
const [filteredQcItems, setFilteredQcItems] = useState<QcItemResult[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [editingItem, setEditingItem] = useState<QcItemResult | null>(null);

useEffect(() => {
loadItems();
}, []);

const loadItems = async () => {
const items = await fetchQcItemsForAll();
setQcItems(items);
setFilteredQcItems(items);
};

const formProps = useForm<SaveQcItemInputs>({
defaultValues: {
code: "",
name: "",
description: "",
},
});

const searchCriteria: Criterion<SearchParamNames>[] = 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) + "<br>";
}
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<Column<QcItemResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: handleEdit,
buttonIcon: <EditNote />,
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: <Delete />,
buttonColor: "error",
},
],
[t, handleEdit, handleDelete]
);

return (
<>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAdd}
>
{t("Create Qc Item")}
</Button>
</Stack>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcItems(
qcItems.filter(
(qi) =>
(!query.code || qi.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qi.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcItemResult>
items={filteredQcItems}
columns={columns}
/>

{/* Add/Edit Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{editingItem ? t("Edit Qc Item") : t("Create Qc Item")}
</DialogTitle>
<FormProvider {...formProps}>
<form onSubmit={formProps.handleSubmit(handleSubmit)}>
<DialogContent>
<QcItemDetails />
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
<Button type="submit" variant="contained">
{t("Submit")}
</Button>
</DialogActions>
</form>
</FormProvider>
</Dialog>
</>
);
};

export default Tab3QcItemManagement;


+ 0
- 6
src/components/Shop/Shop.tsx Ver ficheiro

@@ -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


+ 36
- 50
src/components/Shop/TruckLane.tsx Ver ficheiro

@@ -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<Truck[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [filters, setFilters] = useState<Record<string, string>>({});
const [page, setPage] = useState(0);
@@ -65,32 +65,6 @@ const TruckLane: React.FC = () => {
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
const [snackbarMessage, setSnackbarMessage] = useState<string>("");

useEffect(() => {
const fetchTruckLanes = async () => {
setLoading(true);
setError(null);
try {
const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
// Get unique truckLanceCodes only
const uniqueCodes = new Map<string, Truck>();
(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<string, string>) => {
setFilters(inputs);
setPage(0); // Reset to first page when searching
const handleSearch = async (inputs: Record<string, string>) => {
setLoading(true);
setError(null);
try {
const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
const uniqueCodes = new Map<string, Truck>();
(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 (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
);
}

if (error) {
return (
<Box>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
</Box>
);
}

const criteria: Criterion<SearchParamNames>[] = [
{ 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<string>[]}
onSearch={handleSearch}
onReset={() => {
setTruckData([]);
setFilters({});
}}
/>
@@ -284,7 +259,17 @@ const TruckLane: React.FC = () => {
{t("Add Truck Lane")}
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}

{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
@@ -356,6 +341,7 @@ const TruckLane: React.FC = () => {
rowsPerPageOptions={[5, 10, 25, 50]}
/>
</TableContainer>
)}
</CardContent>
</Card>



+ 2
- 2
src/components/StockIssue/SearchPage.tsx Ver ficheiro

@@ -150,7 +150,7 @@ const SearchPage: React.FC<Props> = ({ 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<Props> = ({ 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"),


+ 4
- 10
src/components/StockRecord/SearchPage.tsx Ver ficheiro

@@ -77,16 +77,10 @@ const SearchPage: React.FC<Props> = ({ 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<Props> = ({ 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,
});
});


+ 11
- 0
src/components/StockTakeManagement/ApproverStockTake.tsx Ver ficheiro

@@ -404,6 +404,17 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
</Box>
) : (
<>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
<TableContainer component={Paper}>
<Table>
<TableHead>


+ 11
- 0
src/components/StockTakeManagement/PickerReStockTake.tsx Ver ficheiro

@@ -354,6 +354,17 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
</Box>
) : (
<>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
<TableContainer component={Paper}>
<Table>
<TableHead>


+ 11
- 0
src/components/StockTakeManagement/PickerStockTake.tsx Ver ficheiro

@@ -443,6 +443,17 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
</Box>
) : (
<>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
<TableContainer component={Paper}>
<Table>
<TableHead>


+ 5
- 0
src/i18n/en/dashboard.json Ver ficheiro

@@ -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",


+ 8
- 1
src/i18n/en/items.json Ver ficheiro

@@ -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"
}

+ 58
- 0
src/i18n/en/qcItemAll.json Ver ficheiro

@@ -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?"
}


+ 6
- 1
src/i18n/zh/dashboard.json Ver ficheiro

@@ -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": "全部",


+ 5
- 0
src/i18n/zh/inventory.json Ver ficheiro

@@ -33,6 +33,11 @@
"Start Time": "開始時間",
"Difference": "差異",
"stockTaking": "盤點中",
"rejected": "已拒絕",
"miss": "缺貨",
"bad": "不良",
"expiry": "過期",
"Bom Req. Qty": "需求數(BOM單位)",
"selected stock take qty": "已選擇盤點數量",
"book qty": "帳面庫存",
"start time": "開始時間",


+ 9
- 2
src/i18n/zh/items.json Ver ficheiro

@@ -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"
}

+ 1
- 0
src/i18n/zh/jo.json Ver ficheiro

@@ -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": "請選擇物料",


+ 58
- 0
src/i18n/zh/qcItemAll.json Ver ficheiro

@@ -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?": "您確定要刪除此項目嗎?"
}


Carregando…
Cancelar
Guardar