Преглед на файлове

update missing item, update FG pick status dashboard

MergeProblem1
Tommy\2Fi-Staff преди 2 седмици
родител
ревизия
289e59d2b5
променени са 14 файла, в които са добавени 403 реда и са изтрити 13 реда
  1. +24
    -1
      src/app/api/bag/action.ts
  2. +5
    -2
      src/app/api/do/actions.tsx
  3. +2
    -2
      src/app/api/do/client.ts
  4. +1
    -0
      src/app/api/settings/item/actions.ts
  5. +28
    -0
      src/app/api/settings/qcCategory/client.ts
  6. +9
    -0
      src/app/api/settings/qcCategory/index.ts
  7. +11
    -0
      src/components/CreateItem/CreateItem.tsx
  8. +57
    -2
      src/components/CreateItem/ProductDetails.tsx
  9. +200
    -0
      src/components/CreateItem/QcItemsList.tsx
  10. +40
    -4
      src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx
  11. +5
    -0
      src/i18n/en/dashboard.json
  12. +8
    -1
      src/i18n/en/items.json
  13. +5
    -0
      src/i18n/zh/dashboard.json
  14. +8
    -1
      src/i18n/zh/items.json

+ 24
- 1
src/app/api/bag/action.ts Целия файл

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

export const fetchBagConsumptions = cache(async (bagLotLineId: number) =>
serverFetchJson<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 Целия файл

@@ -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 Целия файл

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


+ 1
- 0
src/app/api/settings/item/actions.ts Целия файл

@@ -45,6 +45,7 @@ export type CreateItemInputs = {
isEgg?: boolean | undefined;
isFee?: boolean | undefined;
isBag?: boolean | undefined;
qcType?: string | undefined;
};

export const saveItem = async (data: CreateItemInputs) => {


+ 28
- 0
src/app/api/settings/qcCategory/client.ts Целия файл

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

import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { QcItemInfo } from "./index";

export const fetchQcItemsByCategoryId = async (categoryId: number): Promise<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 Целия файл

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


+ 11
- 0
src/components/CreateItem/CreateItem.tsx Целия файл

@@ -31,6 +31,7 @@ import { saveItemQcChecks } from "@/app/api/settings/qcCheck/actions";
import { useGridApiRef } from "@mui/x-data-grid";
import { QcCategoryCombo } from "@/app/api/settings/qcCategory";
import { WarehouseResult } from "@/app/api/warehouse";
import { softDeleteBagByItemId } from "@/app/api/bag/action";

type Props = {
isEditMode: boolean;
@@ -173,6 +174,16 @@ const CreateItem: React.FC<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);
}
}


+ 57
- 2
src/components/CreateItem/ProductDetails.tsx Целия файл

@@ -29,8 +29,10 @@ import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid";
import { TypeEnum } from "@/app/utils/typeEnum";
import { CreateItemInputs } from "@/app/api/settings/item/actions";
import { ItemQc } from "@/app/api/settings/item";
import { QcCategoryCombo } from "@/app/api/settings/qcCategory";
import { QcCategoryCombo, QcItemInfo } from "@/app/api/settings/qcCategory";
import { fetchQcItemsByCategoryId } from "@/app/api/settings/qcCategory/client";
import { WarehouseResult } from "@/app/api/warehouse";
import QcItemsList from "./QcItemsList";
type Props = {
// isEditMode: boolean;
// type: TypeEnum;
@@ -43,11 +45,13 @@ type Props = {
};

const ProductDetails: React.FC<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 Целия файл

@@ -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 Целия файл

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


+ 5
- 0
src/i18n/en/dashboard.json Целия файл

@@ -73,6 +73,11 @@
"Last Ticket End": "Last Ticket End",
"Pick Time (min)": "Pick Time (min)",
"No truck schedules available for today": "No truck schedules available for today",
"No truck schedules available": "No truck schedules available",
"Select Date": "Select Date",
"Today": "Today",
"Tomorrow": "Tomorrow",
"Day After Tomorrow": "Day After Tomorrow",
"Goods Receipt Status": "Goods Receipt Status",
"Filter": "Filter",
"All": "All",


+ 8
- 1
src/i18n/en/items.json Целия файл

@@ -9,5 +9,12 @@
"Back": "Back",
"Status": "Status",
"Complete": "Complete",
"Missing Data": "Missing Data"
"Missing Data": "Missing Data",
"Loading QC items...": "Loading QC items...",
"Select a QC template to view items": "Select a QC template to view items",
"No QC items in this template": "No QC items in this template",
"QC Checklist": "QC Checklist",
"QC Type": "QC Type",
"IPQC": "IPQC",
"EPQC": "EPQC"
}

+ 5
- 0
src/i18n/zh/dashboard.json Целия файл

@@ -73,6 +73,11 @@
"Last Ticket End": "末單結束時間",
"Pick Time (min)": "揀貨時間(分鐘)",
"No truck schedules available for today": "今日無車輛調度計劃",
"No truck schedules available": "無車輛調度計劃",
"Select Date": "請選擇日期",
"Today": "是日",
"Tomorrow": "翌日",
"Day After Tomorrow": "後日",
"Goods Receipt Status": "貨物接收狀態",
"Filter": "篩選",
"All": "全部",


+ 8
- 1
src/i18n/zh/items.json Целия файл

@@ -43,5 +43,12 @@
"Back": "返回",
"Status": "狀態",
"Complete": "完成",
"Missing Data": "缺少資料"
"Missing Data": "缺少資料",
"Loading QC items...": "正在加載質檢項目...",
"Select a QC template to view items": "選擇質檢模板以查看項目",
"No QC items in this template": "此模板無質檢項目",
"QC Checklist": "質檢項目",
"QC Type": "質檢種類",
"IPQC": "IPQC",
"EPQC": "EPQC"
}

Зареждане…
Отказ
Запис