CANCERYS\kw093 3 недель назад
Родитель
Сommit
3417a48ab5
17 измененных файлов: 1319 добавлений и 28 удалений
  1. +9
    -1
      src/app/(main)/settings/qrCodeHandle/page.tsx
  2. +24
    -0
      src/app/api/do/actions.tsx
  3. +16
    -0
      src/app/api/do/client.ts
  4. +33
    -0
      src/app/api/warehouse/client.ts
  5. +1
    -2
      src/components/CreateItem/CreateItem.tsx
  6. +8
    -0
      src/components/DashboardPage/DashboardPage.tsx
  7. +397
    -0
      src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx
  8. +3
    -0
      src/components/DashboardPage/truckSchedule/index.ts
  9. +22
    -2
      src/components/Shop/TruckLaneDetail.tsx
  10. +0
    -20
      src/components/WarehouseHandle/WarehouseHandle.tsx
  11. +14
    -1
      src/components/qrCodeHandles/qrCodeHandleTabs.tsx
  12. +675
    -0
      src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx
  13. +21
    -0
      src/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper.tsx
  14. +3
    -0
      src/i18n/en/common.json
  15. +76
    -1
      src/i18n/en/dashboard.json
  16. +1
    -0
      src/i18n/zh/common.json
  17. +16
    -1
      src/i18n/zh/dashboard.json

+ 9
- 1
src/app/(main)/settings/qrCodeHandle/page.tsx Просмотреть файл

@@ -4,6 +4,7 @@ import Typography from "@mui/material/Typography";
import { getServerI18n } from "@/i18n";
import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper";
import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper";
import QrCodeHandleWarehouseSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper";
import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs";
import { I18nProvider } from "@/i18n";
import Box from "@mui/material/Box";
@@ -19,7 +20,7 @@ const QrCodeHandlePage: React.FC = async () => {
{t("QR Code Handle")}
</Typography>
<I18nProvider namespaces={["common", "user"]}>
<I18nProvider namespaces={["common", "user", "warehouse"]}>
<QrCodeHandleTabs
userTabContent={
<Suspense fallback={<QrCodeHandleSearchWrapper.Loading />}>
@@ -35,6 +36,13 @@ const QrCodeHandlePage: React.FC = async () => {
</I18nProvider>
</Suspense>
}
warehouseTabContent={
<Suspense fallback={<QrCodeHandleWarehouseSearchWrapper.Loading />}>
<I18nProvider namespaces={["warehouse", "common", "dashboard"]}>
<QrCodeHandleWarehouseSearchWrapper />
</I18nProvider>
</Suspense>
}
/>
</I18nProvider>
</Box>


+ 24
- 0
src/app/api/do/actions.tsx Просмотреть файл

@@ -131,6 +131,21 @@ export interface getTicketReleaseTable {
handlerName: string | null;
numberOfFGItems: number;
}

export interface TruckScheduleDashboardItem {
storeId: string | null;
truckId: number | null;
truckLanceCode: string | null;
truckDepartureTime: string | number[] | null;
numberOfShopsToServe: number;
numberOfPickTickets: number;
totalItemsToPick: number;
numberOfTicketsReleased: number;
firstTicketStartTime: string | number[] | null;
numberOfTicketsCompleted: number;
lastTicketEndTime: string | number[] | null;
pickTimeTakenMinutes: number | null;
}
export interface SearchDeliveryOrderInfoRequest {
code: string;
shopName: string;
@@ -181,6 +196,15 @@ export const fetchTicketReleaseTable = cache(async (startDate: string, endDate:
}
);
});

export const fetchTruckScheduleDashboard = cache(async () => {
return await serverFetchJson<TruckScheduleDashboardItem[]>(
`${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`,
{
method: "GET",
}
);
});
export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => {
const { doId, userId } = data;
return await serverFetchJson<{ id: number|null; code: string; entity?: any }>(


+ 16
- 0
src/app/api/do/client.ts Просмотреть файл

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

import {
fetchTruckScheduleDashboard,
type TruckScheduleDashboardItem
} from "./actions";

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

export type { TruckScheduleDashboardItem };

export default fetchTruckScheduleDashboardClient;



+ 33
- 0
src/app/api/warehouse/client.ts Просмотреть файл

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

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

export const exportWarehouseQrCode = async (warehouseIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => {

const token = localStorage.getItem("accessToken");
const response = await fetch(`${NEXT_PUBLIC_API_URL}/warehouse/export-qrcode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
},
body: JSON.stringify({ warehouseIds }),
});

if (!response.ok) {
if (response.status === 401) {
throw new Error("Unauthorized: Please log in again");
}
throw new Error(`Failed to export QR code: ${response.status} ${response.statusText}`);
}

const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "warehouse_qrcode.pdf";
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const blobValue = new Uint8Array(arrayBuffer);

return { blobValue, filename };
};

+ 1
- 2
src/components/CreateItem/CreateItem.tsx Просмотреть файл

@@ -159,9 +159,8 @@ const CreateItem: React.FC<Props> = ({
console.log(qcCheck);
// return
// do api
console.log("asdad");
const responseI = await saveItem(data);
console.log("asdad");
const responseQ = await saveItemQcChecks(qcCheck);
if (responseI && responseQ) {
if (!Boolean(responseI.id)) {


+ 8
- 0
src/components/DashboardPage/DashboardPage.tsx Просмотреть файл

@@ -17,6 +17,7 @@ import CollapsibleCard from "../CollapsibleCard";
// import SupervisorQcApproval, { IQCItems } from "./QC/SupervisorQcApproval";
import { EscalationResult } from "@/app/api/escalation";
import EscalationLogTable from "./escalation/EscalationLogTable";
import { TruckScheduleDashboard } from "./truckSchedule";
type Props = {
// iqc: IQCItems[] | undefined
escalationLogs: EscalationResult[]
@@ -42,6 +43,13 @@ const DashboardPage: React.FC<Props> = ({
return (
<ThemeProvider theme={theme}>
<Grid container spacing={2}>
<Grid item xs={12}>
<CollapsibleCard title={t("Truck Schedule Dashboard")} defaultOpen={true}>
<CardContent>
<TruckScheduleDashboard />
</CardContent>
</CollapsibleCard>
</Grid>
<Grid item xs={12}>
<CollapsibleCard
title={`${t("Responsible Escalation List")} (${t("pending")} : ${


+ 397
- 0
src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx Просмотреть файл

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

import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Box,
Typography,
FormControl,
InputLabel,
Select,
MenuItem,
Card,
CardContent,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
CircularProgress,
Chip
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { fetchTruckScheduleDashboardClient, type TruckScheduleDashboardItem } from '@/app/api/do/client';
import { formatDepartureTime, arrayToDayjs } from '@/app/utils/formatUtil';

// Track completed items for hiding after 2 refresh cycles
interface CompletedTracker {
key: string;
refreshCount: number;
}

const TruckScheduleDashboard: React.FC = () => {
const { t } = useTranslation("dashboard");
const [selectedStore, setSelectedStore] = useState<string>("");
const [data, setData] = useState<TruckScheduleDashboardItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// Initialize as null to avoid SSR/client hydration mismatch
const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null);
const [isClient, setIsClient] = useState<boolean>(false);
const completedTrackerRef = useRef<Map<string, CompletedTracker>>(new Map());
const refreshCountRef = useRef<number>(0);
// Set client flag and time on mount
useEffect(() => {
setIsClient(true);
setCurrentTime(dayjs());
}, []);

// Format time from array or string to HH:mm
const formatTime = (timeData: string | number[] | null): string => {
if (!timeData) return '-';
if (Array.isArray(timeData)) {
if (timeData.length >= 2) {
const hour = timeData[0] || 0;
const minute = timeData[1] || 0;
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
}
return '-';
}
if (typeof timeData === 'string') {
const parts = timeData.split(':');
if (parts.length >= 2) {
const hour = parseInt(parts[0], 10);
const minute = parseInt(parts[1], 10);
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
}
}
return '-';
};

// Format datetime from array or string
const formatDateTime = (dateTimeData: string | number[] | null): string => {
if (!dateTimeData) return '-';
if (Array.isArray(dateTimeData)) {
return arrayToDayjs(dateTimeData, true).format('HH:mm');
}
const parsed = dayjs(dateTimeData);
if (parsed.isValid()) {
return parsed.format('HH:mm');
}
return '-';
};

// Calculate time remaining for truck departure
const calculateTimeRemaining = useCallback((departureTime: string | number[] | null): string => {
if (!departureTime || !currentTime) return '-';
const now = currentTime;
let departureHour: number;
let departureMinute: number;
if (Array.isArray(departureTime)) {
if (departureTime.length < 2) return '-';
departureHour = departureTime[0] || 0;
departureMinute = departureTime[1] || 0;
} else if (typeof departureTime === 'string') {
const parts = departureTime.split(':');
if (parts.length < 2) return '-';
departureHour = parseInt(parts[0], 10);
departureMinute = parseInt(parts[1], 10);
} else {
return '-';
}
// Create departure datetime for today
const departure = now.clone().hour(departureHour).minute(departureMinute).second(0);
const diffMinutes = departure.diff(now, 'minute');
if (diffMinutes < 0) {
// Past departure time
const absDiff = Math.abs(diffMinutes);
const hours = Math.floor(absDiff / 60);
const minutes = absDiff % 60;
return `-${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
} else {
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
}, [currentTime]);

// Generate unique key for tracking completed items
const getItemKey = (item: TruckScheduleDashboardItem): string => {
return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`;
};

// Load data from API
const loadData = useCallback(async () => {
try {
const result = await fetchTruckScheduleDashboardClient();
// Update completed tracker
refreshCountRef.current += 1;
const currentRefresh = refreshCountRef.current;
result.forEach(item => {
const key = getItemKey(item);
// If all tickets are completed, track it
if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) {
const existing = completedTrackerRef.current.get(key);
if (!existing) {
completedTrackerRef.current.set(key, { key, refreshCount: currentRefresh });
}
} else {
// Remove from tracker if no longer completed
completedTrackerRef.current.delete(key);
}
});
// Filter out items that have been completed for 2+ refresh cycles
const filteredResult = result.filter(item => {
const key = getItemKey(item);
const tracker = completedTrackerRef.current.get(key);
if (tracker) {
// Hide if completed for 2 or more refresh cycles
if (currentRefresh - tracker.refreshCount >= 2) {
return false;
}
}
return true;
});
setData(filteredResult);
} catch (error) {
console.error('Error fetching truck schedule dashboard:', error);
} finally {
setLoading(false);
}
}, []);

// Initial load and auto-refresh every 5 minutes
useEffect(() => {
loadData();
const refreshInterval = setInterval(() => {
loadData();
}, 5 * 60 * 1000); // 5 minutes
return () => clearInterval(refreshInterval);
}, [loadData]);

// Update current time every 1 minute for time remaining calculation
useEffect(() => {
if (!isClient) return;
const timeInterval = setInterval(() => {
setCurrentTime(dayjs());
}, 60 * 1000); // 1 minute
return () => clearInterval(timeInterval);
}, [isClient]);

// Filter data by selected store
const filteredData = useMemo(() => {
if (!selectedStore) return data;
return data.filter(item => item.storeId === selectedStore);
}, [data, selectedStore]);

// Get chip color based on time remaining
const getTimeChipColor = (departureTime: string | number[] | null): "success" | "warning" | "error" | "default" => {
if (!departureTime || !currentTime) return "default";
const now = currentTime;
let departureHour: number;
let departureMinute: number;
if (Array.isArray(departureTime)) {
if (departureTime.length < 2) return "default";
departureHour = departureTime[0] || 0;
departureMinute = departureTime[1] || 0;
} else if (typeof departureTime === 'string') {
const parts = departureTime.split(':');
if (parts.length < 2) return "default";
departureHour = parseInt(parts[0], 10);
departureMinute = parseInt(parts[1], 10);
} else {
return "default";
}
const departure = now.clone().hour(departureHour).minute(departureMinute).second(0);
const diffMinutes = departure.diff(now, 'minute');
if (diffMinutes < 0) return "error"; // Past due
if (diffMinutes <= 30) return "warning"; // Within 30 minutes
return "success"; // More than 30 minutes
};

return (
<Card sx={{ mb: 2 }}>
<CardContent>
{/* Title */}
<Typography variant="h5" sx={{ mb: 3, fontWeight: 600 }}>
{t("Truck Schedule Dashboard")}
</Typography>

{/* Filter */}
<Stack direction="row" spacing={2} sx={{ mb: 3 }}>
<FormControl sx={{ minWidth: 150 }} size="small">
<InputLabel id="store-select-label" shrink={true}>
{t("Store ID")}
</InputLabel>
<Select
labelId="store-select-label"
id="store-select"
value={selectedStore}
label={t("Store ID")}
onChange={(e) => setSelectedStore(e.target.value)}
displayEmpty
>
<MenuItem value="">{t("All Stores")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</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') : '--:--:--'}
</Typography>
</Stack>

{/* Table */}
<Box sx={{ mt: 2 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table size="small" sx={{ minWidth: 1200 }}>
<TableHead>
<TableRow sx={{ backgroundColor: 'grey.100' }}>
<TableCell sx={{ fontWeight: 600 }}>{t("Store ID")}</TableCell>
<TableCell sx={{ fontWeight: 600 }}>{t("Truck Schedule")}</TableCell>
<TableCell sx={{ fontWeight: 600 }}>{t("Time Remaining")}</TableCell>
<TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Shops")}</TableCell>
<TableCell sx={{ fontWeight: 600 }} align="center">{t("Total Items")}</TableCell>
<TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Released")}</TableCell>
<TableCell sx={{ fontWeight: 600 }}>{t("First Ticket Start")}</TableCell>
<TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Completed")}</TableCell>
<TableCell sx={{ fontWeight: 600 }}>{t("Last Ticket End")}</TableCell>
<TableCell sx={{ fontWeight: 600 }} align="center">{t("Pick Time (min)")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredData.length === 0 ? (
<TableRow>
<TableCell colSpan={10} align="center">
<Typography variant="body2" color="text.secondary">
{t("No truck schedules available for today")}
</Typography>
</TableCell>
</TableRow>
) : (
filteredData.map((row, index) => {
const timeRemaining = calculateTimeRemaining(row.truckDepartureTime);
const chipColor = getTimeChipColor(row.truckDepartureTime);
return (
<TableRow
key={`${row.storeId}-${row.truckLanceCode}-${index}`}
sx={{
'&:hover': { backgroundColor: 'grey.50' },
backgroundColor: row.numberOfPickTickets > 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets
? 'success.light'
: 'inherit'
}}
>
<TableCell>
<Chip
label={row.storeId || '-'}
size="small"
color={row.storeId === '2/F' ? 'primary' : 'secondary'}
/>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{row.truckLanceCode || '-'}
</Typography>
<Typography variant="caption" color="text.secondary">
ETD: {formatTime(row.truckDepartureTime)}
</Typography>
</Box>
</TableCell>
<TableCell>
<Chip
label={timeRemaining}
size="small"
color={chipColor}
sx={{ fontWeight: 600 }}
/>
</TableCell>
<TableCell align="center">
<Typography variant="body2">
{row.numberOfShopsToServe} [{row.numberOfPickTickets}]
</Typography>
</TableCell>
<TableCell align="center">
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{row.totalItemsToPick}
</Typography>
</TableCell>
<TableCell align="center">
<Chip
label={row.numberOfTicketsReleased}
size="small"
color={row.numberOfTicketsReleased > 0 ? 'info' : 'default'}
/>
</TableCell>
<TableCell>
{formatDateTime(row.firstTicketStartTime)}
</TableCell>
<TableCell align="center">
<Chip
label={row.numberOfTicketsCompleted}
size="small"
color={row.numberOfTicketsCompleted > 0 ? 'success' : 'default'}
/>
</TableCell>
<TableCell>
{formatDateTime(row.lastTicketEndTime)}
</TableCell>
<TableCell align="center">
<Typography
variant="body2"
sx={{
fontWeight: 500,
color: row.pickTimeTakenMinutes !== null ? 'text.primary' : 'text.secondary'
}}
>
{row.pickTimeTakenMinutes !== null ? row.pickTimeTakenMinutes : '-'}
</Typography>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</CardContent>
</Card>
);
};

export default TruckScheduleDashboard;

+ 3
- 0
src/components/DashboardPage/truckSchedule/index.ts Просмотреть файл

@@ -0,0 +1,3 @@
export { default as TruckScheduleDashboard } from './TruckScheduleDashboard';



+ 22
- 2
src/components/Shop/TruckLaneDetail.tsx Просмотреть файл

@@ -69,6 +69,7 @@ const TruckLaneDetail: React.FC = () => {
const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]);
const [uniqueShopCodes, setUniqueShopCodes] = useState<string[]>([]);
const [uniqueShopNames, setUniqueShopNames] = useState<string[]>([]);
const [shopNameByCodeMap, setShopNameByCodeMap] = useState<Map<string, string>>(new Map());
const [addShopDialogOpen, setAddShopDialogOpen] = useState<boolean>(false);
const [newShop, setNewShop] = useState({
shopName: "",
@@ -86,11 +87,12 @@ const TruckLaneDetail: React.FC = () => {
useEffect(() => {
const fetchAutocompleteData = async () => {
try {
const [shopData, remarks, codes, names] = await Promise.all([
const [shopData, remarks, codes, names, allShopsFromShopTable] = await Promise.all([
findAllUniqueShopNamesAndCodesFromTrucksClient() as Promise<Array<{ name: string; code: string }>>,
findAllUniqueRemarksFromTrucksClient() as Promise<string[]>,
findAllUniqueShopCodesFromTrucksClient() as Promise<string[]>,
findAllUniqueShopNamesFromTrucksClient() as Promise<string[]>,
fetchAllShopsClient() as Promise<ShopAndTruck[]>,
]);

// Convert to Shop format (id will be 0 since we don't have shop IDs from truck table)
@@ -105,6 +107,15 @@ const TruckLaneDetail: React.FC = () => {
setUniqueRemarks(remarks || []);
setUniqueShopCodes(codes || []);
setUniqueShopNames(names || []);

// Create lookup map: shopCode -> shopName from shop table
const shopNameMap = new Map<string, string>();
(allShopsFromShopTable || []).forEach((shop) => {
if (shop.code) {
shopNameMap.set(String(shop.code).trim().toLowerCase(), String(shop.name || "").trim());
}
});
setShopNameByCodeMap(shopNameMap);
} catch (err) {
console.error("Failed to load autocomplete data:", err);
}
@@ -700,6 +711,7 @@ const TruckLaneDetail: React.FC = () => {
<TableHead>
<TableRow>
<TableCell>{t("Shop Name")}</TableCell>
<TableCell>{t("Shop Branch")}</TableCell>
<TableCell>{t("Shop Code")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Loading Sequence")}</TableCell>
@@ -709,7 +721,7 @@ const TruckLaneDetail: React.FC = () => {
<TableBody>
{shopsData.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
<TableCell colSpan={6} align="center">
<Typography variant="body2" color="text.secondary">
{t("No shops found using this truck lane")}
</Typography>
@@ -719,6 +731,14 @@ const TruckLaneDetail: React.FC = () => {
shopsData.map((shop, index) => (
<TableRow key={shop.id ?? `shop-${index}`}>
<TableCell>
{/* Shop Name from shop table (read-only, looked up by shop code) */}
{(() => {
const shopCode = String(shop.code || "").trim().toLowerCase();
return shopNameByCodeMap.get(shopCode) || "-";
})()}
</TableCell>
<TableCell>
{/* Shop Branch from truck table (editable) */}
{editingRowIndex === index ? (
<Autocomplete
freeSolo


+ 0
- 20
src/components/WarehouseHandle/WarehouseHandle.tsx Просмотреть файл

@@ -53,8 +53,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
successDialog(t("Delete Success"), t);
} catch (error) {
console.error("Failed to delete warehouse:", error);
// Don't redirect on error, just show error message
// The error will be logged but user stays on the page
}
}, t);
}, [t, router]);
@@ -76,18 +74,14 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
try {
let results: WarehouseResult[] = warehouses;

// Build search pattern from the four fields: store_idF-warehouse-area-slot
// Only search by code field - match the code that follows this pattern
const storeId = searchInputs.store_id?.trim() || "";
const warehouse = searchInputs.warehouse?.trim() || "";
const area = searchInputs.area?.trim() || "";
const slot = searchInputs.slot?.trim() || "";
const stockTakeSection = searchInputs.stockTakeSection?.trim() || "";

// If any field has a value, filter by code pattern and stockTakeSection
if (storeId || warehouse || area || slot || stockTakeSection) {
results = warehouses.filter((warehouseItem) => {
// Filter by stockTakeSection if provided
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) {
@@ -95,7 +89,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
}
}
// Filter by code pattern if any code-related field is provided
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
@@ -103,8 +96,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
const codeValue = String(warehouseItem.code).toLowerCase();
// Check if code matches the pattern: store_id-warehouse-area-slot
// Match each part if provided
const codeParts = codeValue.split("-");
if (codeParts.length >= 4) {
@@ -121,7 +112,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
// Fallback: if code doesn't follow the pattern, check if it contains any of the search terms
const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase());
const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase());
const areaMatch = !area || codeValue.includes(area.toLowerCase());
@@ -130,11 +120,9 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
// If only stockTakeSection is provided, return true (already filtered above)
return true;
});
} else {
// If no search terms, show all warehouses
results = warehouses;
}

@@ -142,7 +130,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
} catch (error) {
console.error("Error searching warehouses:", error);
// Fallback: filter by code pattern and stockTakeSection
const storeId = searchInputs.store_id?.trim().toLowerCase() || "";
const warehouse = searchInputs.warehouse?.trim().toLowerCase() || "";
const area = searchInputs.area?.trim().toLowerCase() || "";
@@ -151,7 +138,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
setFilteredWarehouse(
warehouses.filter((warehouseItem) => {
// Filter by stockTakeSection if provided
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
if (!itemStockTakeSection.includes(stockTakeSection)) {
@@ -159,7 +145,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
}
}
// Filter by code if any code-related field is provided
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
@@ -267,7 +252,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
justifyContent: "flex-start",
}}
>
{/* 樓層 field with F inside on the right */}
<TextField
label={t("store_id")}
value={searchInputs.store_id}
@@ -285,7 +269,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
{/* 倉庫 field */}
<TextField
label={t("warehouse")}
value={searchInputs.warehouse}
@@ -298,7 +281,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
{/* 區域 field */}
<TextField
label={t("area")}
value={searchInputs.area}
@@ -311,7 +293,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
{/* 儲位 field */}
<TextField
label={t("slot")}
value={searchInputs.slot}
@@ -321,7 +302,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
{/* 盤點區域 field */}
<Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}>
<TextField
label={t("stockTakeSection")}


+ 14
- 1
src/components/qrCodeHandles/qrCodeHandleTabs.tsx Просмотреть файл

@@ -30,20 +30,24 @@ function TabPanel(props: TabPanelProps) {
interface QrCodeHandleTabsProps {
userTabContent: ReactNode;
equipmentTabContent: ReactNode;
warehouseTabContent: ReactNode;
}

const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
userTabContent,
equipmentTabContent,
warehouseTabContent,
}) => {
const { t } = useTranslation("common");
const { t: tUser } = useTranslation("user");
const { t: tWarehouse } = useTranslation("warehouse");
const searchParams = useSearchParams();
const router = useRouter();
const getInitialTab = () => {
const tab = searchParams.get("tab");
if (tab === "equipment") return 1;
if (tab === "warehouse") return 2;
if (tab === "user") return 0;
return 0;
};
@@ -54,6 +58,8 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
const tab = searchParams.get("tab");
if (tab === "equipment") {
setCurrentTab(1);
} else if (tab === "warehouse") {
setCurrentTab(2);
} else if (tab === "user") {
setCurrentTab(0);
}
@@ -61,7 +67,9 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue);
const tabName = newValue === 1 ? "equipment" : "user";
let tabName = "user";
if (newValue === 1) tabName = "equipment";
else if (newValue === 2) tabName = "warehouse";
const params = new URLSearchParams(searchParams.toString());
params.set("tab", tabName);
router.push(`?${params.toString()}`, { scroll: false });
@@ -73,6 +81,7 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={tUser("User")} />
<Tab label={t("Equipment")} />
<Tab label={tWarehouse("Warehouse")} />
</Tabs>
</Box>

@@ -83,6 +92,10 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
<TabPanel value={currentTab} index={1}>
{equipmentTabContent}
</TabPanel>

<TabPanel value={currentTab} index={2}>
{warehouseTabContent}
</TabPanel>
</Box>
);
};


+ 675
- 0
src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx Просмотреть файл

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

import { useCallback, useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import { successDialog } from "../Swal/CustomAlerts";
import useUploadContext from "../UploadProvider/useUploadContext";
import { downloadFile } from "@/app/utils/commonUtil";
import { WarehouseResult } from "@/app/api/warehouse";
import { exportWarehouseQrCode } from "@/app/api/warehouse/client";
import {
Checkbox,
Box,
Button,
TextField,
Stack,
Autocomplete,
Modal,
Card,
CardContent,
CardActions,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Typography,
InputAdornment
} from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
import PrintIcon from "@mui/icons-material/Print";
import CloseIcon from "@mui/icons-material/Close";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Search from "@mui/icons-material/Search";
import { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
warehouses: WarehouseResult[];
printerCombo: PrinterCombo[];
}

const QrCodeHandleWarehouseSearch: React.FC<Props> = ({ warehouses, printerCombo }) => {
const { t } = useTranslation(["warehouse", "common"]);
const [filteredWarehouses, setFilteredWarehouses] = useState(warehouses);
const { setIsUploading } = useUploadContext();
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
});

const [checkboxIds, setCheckboxIds] = useState<number[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [printQty, setPrintQty] = useState(1);
const [isSearching, setIsSearching] = useState(false);

const [previewOpen, setPreviewOpen] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);

const [selectedWarehousesModalOpen, setSelectedWarehousesModalOpen] = useState(false);

const [searchInputs, setSearchInputs] = useState({
store_id: "",
warehouse: "",
area: "",
slot: "",
});

const filteredPrinters = useMemo(() => {
return printerCombo.filter((printer) => {
return printer.type === "A4";
});
}, [printerCombo]);

const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
filteredPrinters.length > 0 ? filteredPrinters[0] : undefined
);

useEffect(() => {
if (!selectedPrinter || !filteredPrinters.find(p => p.id === selectedPrinter.id)) {
setSelectedPrinter(filteredPrinters.length > 0 ? filteredPrinters[0] : undefined);
}
}, [filteredPrinters, selectedPrinter]);

const handleReset = useCallback(() => {
setSearchInputs({
store_id: "",
warehouse: "",
area: "",
slot: "",
});
setFilteredWarehouses(warehouses);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
}, [warehouses, pagingController.pageSize]);

const handleSearch = useCallback(() => {
setIsSearching(true);
try {
let results: WarehouseResult[] = warehouses;

const storeId = searchInputs.store_id?.trim() || "";
const warehouse = searchInputs.warehouse?.trim() || "";
const area = searchInputs.area?.trim() || "";
const slot = searchInputs.slot?.trim() || "";

if (storeId || warehouse || area || slot) {
results = warehouses.filter((warehouseItem) => {
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
}
const codeValue = String(warehouseItem.code).toLowerCase();
const codeParts = codeValue.split("-");
if (codeParts.length >= 4) {
const codeStoreId = codeParts[0] || "";
const codeWarehouse = codeParts[1] || "";
const codeArea = codeParts[2] || "";
const codeSlot = codeParts[3] || "";
const storeIdMatch = !storeId || codeStoreId.includes(storeId.toLowerCase());
const warehouseMatch = !warehouse || codeWarehouse.includes(warehouse.toLowerCase());
const areaMatch = !area || codeArea.includes(area.toLowerCase());
const slotMatch = !slot || codeSlot.includes(slot.toLowerCase());
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase());
const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase());
const areaMatch = !area || codeValue.includes(area.toLowerCase());
const slotMatch = !slot || codeValue.includes(slot.toLowerCase());
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
return true;
});
} else {
results = warehouses;
}

setFilteredWarehouses(results);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
} catch (error) {
console.error("Error searching warehouses:", error);
const storeId = searchInputs.store_id?.trim().toLowerCase() || "";
const warehouse = searchInputs.warehouse?.trim().toLowerCase() || "";
const area = searchInputs.area?.trim().toLowerCase() || "";
const slot = searchInputs.slot?.trim().toLowerCase() || "";
setFilteredWarehouses(
warehouses.filter((warehouseItem) => {
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
}
const codeValue = String(warehouseItem.code).toLowerCase();
const codeParts = codeValue.split("-");
if (codeParts.length >= 4) {
const storeIdMatch = !storeId || codeParts[0].includes(storeId);
const warehouseMatch = !warehouse || codeParts[1].includes(warehouse);
const areaMatch = !area || codeParts[2].includes(area);
const slotMatch = !slot || codeParts[3].includes(slot);
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
return (!storeId || codeValue.includes(storeId)) &&
(!warehouse || codeValue.includes(warehouse)) &&
(!area || codeValue.includes(area)) &&
(!slot || codeValue.includes(slot));
}
return true;
})
);
} finally {
setIsSearching(false);
}
}, [searchInputs, warehouses, pagingController.pageSize]);

const handleSelectWarehouse = useCallback((warehouseId: number, checked: boolean) => {
if (checked) {
setCheckboxIds(prev => [...prev, warehouseId]);
} else {
setCheckboxIds(prev => prev.filter(id => id !== warehouseId));
setSelectAll(false);
}
}, []);

const handleSelectAll = useCallback((checked: boolean) => {
if (checked) {
setCheckboxIds(filteredWarehouses.map(warehouse => warehouse.id));
setSelectAll(true);
} else {
setCheckboxIds([]);
setSelectAll(false);
}
}, [filteredWarehouses]);

const showPdfPreview = useCallback(async (warehouseIds: number[]) => {
if (warehouseIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportWarehouseQrCode(warehouseIds);
const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
setPreviewUrl(`${url}#toolbar=0`);
setPreviewOpen(true);
} catch (error) {
console.error("Error exporting QR code:", error);
} finally {
setIsUploading(false);
}
}, [setIsUploading]);

const handleClosePreview = useCallback(() => {
setPreviewOpen(false);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
}, [previewUrl]);

const handleDownloadQrCode = useCallback(async (warehouseIds: number[]) => {
if (warehouseIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportWarehouseQrCode(warehouseIds);
downloadFile(response.blobValue, response.filename);
setSelectedWarehousesModalOpen(false);
successDialog("二維碼已下載", t);
} catch (error) {
console.error("Error exporting QR code:", error);
} finally {
setIsUploading(false);
}
}, [setIsUploading, t]);

const handlePrint = useCallback(async () => {
if (checkboxIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportWarehouseQrCode(checkboxIds);
const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.onload = () => {
for (let i = 0; i < printQty; i++) {
setTimeout(() => {
printWindow.print();
}, i * 500);
}
};
}
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
setSelectedWarehousesModalOpen(false);
successDialog("二維碼已列印", t);
} catch (error) {
console.error("Error printing QR code:", error);
} finally {
setIsUploading(false);
}
}, [checkboxIds, printQty, setIsUploading, t]);

const handleViewSelectedQrCodes = useCallback(() => {
if (checkboxIds.length === 0) {
return;
}
setSelectedWarehousesModalOpen(true);
}, [checkboxIds]);

const selectedWarehouses = useMemo(() => {
return warehouses.filter(warehouse => checkboxIds.includes(warehouse.id));
}, [warehouses, checkboxIds]);

const handleCloseSelectedWarehousesModal = useCallback(() => {
setSelectedWarehousesModalOpen(false);
}, []);

const columns = useMemo<Column<WarehouseResult>[]>(
() => [
{
name: "id",
label: "",
sx: { width: "50px", minWidth: "50px" },
renderCell: (params) => (
<Checkbox
checked={checkboxIds.includes(params.id)}
onChange={(e) => handleSelectWarehouse(params.id, e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
),
},
{
name: "code",
label: t("code"),
align: "left",
headerAlign: "left",
sx: { width: "200px", minWidth: "200px" },
},
{
name: "store_id",
label: t("store_id"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
{
name: "warehouse",
label: t("warehouse"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
{
name: "area",
label: t("area"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
{
name: "slot",
label: t("slot"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
],
[t, checkboxIds, handleSelectWarehouse],
);

return (
<>
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "nowrap",
justifyContent: "flex-start",
}}
>
<TextField
label={t("store_id")}
value={searchInputs.store_id}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, store_id: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
InputProps={{
endAdornment: (
<InputAdornment position="end">F</InputAdornment>
),
}}
/>
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
<TextField
label={t("warehouse")}
value={searchInputs.warehouse}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, warehouse: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
<TextField
label={t("area")}
value={searchInputs.area}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, area: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
<TextField
label={t("slot")}
value={searchInputs.slot}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, slot: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
</Box>
<CardActions sx={{ justifyContent: "flex-start", px: 0, pt: 1 }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
<SearchResults<WarehouseResult>
items={filteredWarehouses}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={filteredWarehouses.length}
isAutoPaging={true}
/>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
variant="outlined"
onClick={() => handleSelectAll(!selectAll)}
startIcon={<Checkbox checked={selectAll} />}
>
選擇全部倉庫 ({checkboxIds.length} / {filteredWarehouses.length})
</Button>
<Button
variant="contained"
onClick={handleViewSelectedQrCodes}
disabled={checkboxIds.length === 0}
color="primary"
>
查看已選擇倉庫二維碼 ({checkboxIds.length})
</Button>
</Box>

<Modal
open={selectedWarehousesModalOpen}
onClose={handleCloseSelectedWarehousesModal}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Card
sx={{
position: 'relative',
width: '90%',
maxWidth: '800px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
outline: 'none',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 2,
borderBottom: 1,
borderColor: 'divider',
}}
>
<Typography variant="h6" component="h2">
已選擇倉庫 ({selectedWarehouses.length})
</Typography>
<IconButton onClick={handleCloseSelectedWarehousesModal}>
<CloseIcon />
</IconButton>
</Box>

<Box
sx={{
flex: 1,
overflow: 'auto',
p: 2,
}}
>
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>
<strong>{t("code")}</strong>
</TableCell>
<TableCell>
<strong>{t("store_id")}</strong>
</TableCell>
<TableCell>
<strong>{t("warehouse")}</strong>
</TableCell>
<TableCell>
<strong>{t("area")}</strong>
</TableCell>
<TableCell>
<strong>{t("slot")}</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{selectedWarehouses.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
沒有選擇的倉庫
</TableCell>
</TableRow>
) : (
selectedWarehouses.map((warehouse) => (
<TableRow key={warehouse.id}>
<TableCell>{warehouse.code || '-'}</TableCell>
<TableCell>{warehouse.store_id || '-'}</TableCell>
<TableCell>{warehouse.warehouse || '-'}</TableCell>
<TableCell>{warehouse.area || '-'}</TableCell>
<TableCell>{warehouse.slot || '-'}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Box>

<Box
sx={{
p: 2,
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.paper',
}}
>
<Stack direction="row" justifyContent="flex-end" alignItems="center" gap={2}>
<Autocomplete<PrinterCombo>
options={filteredPrinters}
value={selectedPrinter ?? null}
onChange={(event, value) => {
setSelectedPrinter(value ?? undefined);
}}
getOptionLabel={(option) => option.name || option.label || option.code || String(option.id)}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="列印機"
sx={{ width: 300 }}
/>
)}
/>
<TextField
variant="outlined"
label="列印數量"
type="number"
value={printQty}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setPrintQty(Math.max(1, value));
}}
inputProps={{ min: 1 }}
sx={{ width: 120 }}
/>
<Button
variant="contained"
startIcon={<PrintIcon />}
onClick={handlePrint}
disabled={checkboxIds.length === 0 || filteredPrinters.length === 0}
color="primary"
>
列印
</Button>
<Button
variant="contained"
startIcon={<DownloadIcon />}
onClick={() => handleDownloadQrCode(checkboxIds)}
disabled={checkboxIds.length === 0}
color="primary"
>
下載二維碼
</Button>
</Stack>
</Box>
</Card>
</Modal>

<Modal
open={previewOpen}
onClose={handleClosePreview}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Card
sx={{
position: 'relative',
width: '90%',
maxWidth: '900px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
outline: 'none',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
p: 2,
borderBottom: 1,
borderColor: 'divider',
}}
>
<IconButton
onClick={handleClosePreview}
>
<CloseIcon />
</IconButton>
</Box>

<Box
sx={{
flex: 1,
overflow: 'auto',
p: 2,
}}
>
{previewUrl && (
<iframe
src={previewUrl}
width="100%"
height="600px"
style={{
border: 'none',
}}
title="PDF Preview"
/>
)}
</Box>
</Card>
</Modal>
</>
);
};

export default QrCodeHandleWarehouseSearch;

+ 21
- 0
src/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper.tsx Просмотреть файл

@@ -0,0 +1,21 @@
import React from "react";
import QrCodeHandleWarehouseSearch from "./qrCodeHandleWarehouseSearch";
import QrCodeHandleSearchLoading from "./qrCodeHandleSearchLoading";
import { fetchWarehouseList } from "@/app/api/warehouse";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

interface SubComponents {
Loading: typeof QrCodeHandleSearchLoading;
}

const QrCodeHandleWarehouseSearchWrapper: React.FC & SubComponents = async () => {
const [warehouses, printerCombo] = await Promise.all([
fetchWarehouseList(),
fetchPrinterCombo(),
]);
return <QrCodeHandleWarehouseSearch warehouses={warehouses} printerCombo={printerCombo} />;
};

QrCodeHandleWarehouseSearchWrapper.Loading = QrCodeHandleSearchLoading;

export default QrCodeHandleWarehouseSearchWrapper;

+ 3
- 0
src/i18n/en/common.json Просмотреть файл

@@ -26,6 +26,9 @@
"Shop added to truck lane successfully": "Shop added to truck lane successfully",
"Failed to create shop in truck lane": "Failed to create shop in truck lane",
"Add Shop": "Add Shop",
"Shop Name": "Shop Name",
"Shop Branch": "Shop Branch",
"Shop Code": "Shop Code",
"Search or select shop name": "Search or select shop name",
"Search or select shop code": "Search or select shop code",
"Search or select remark": "Search or select remark",


+ 76
- 1
src/i18n/en/dashboard.json Просмотреть файл

@@ -1 +1,76 @@
{}
{
"Dashboard": "Dashboard",
"Order status": "Order status",
"pending": "pending",
"receiving": "receiving",
"total": "total",
"Warehouse temperature record": "Warehouse temperature record",
"Warehouse type": "Warehouse type",
"Last 6 hours": "Last 6 hours",
"Add some entries!": "Add some entries!",
"Last 24 hours": "Last 24 hours",
"Cold storage": "Cold storage",
"Normal temperature storage": "Normal temperature storage",
"Temperature status": "Temperature status",
"Humidity status": "Humidity status",
"Warehouse status": "Warehouse status",
"Progress chart": "Progress chart",
"Purchase Order Code": "Purchase Order Code",
"Item Name": "Item Name",
"Escalation Level": "Escalation Level",
"Reason": "Reason",
"escalated date": "escalated date",
"Order completion": "Order completion",
"Store Management": "Store Management",
"Consumable": "Consumable",
"Shipment": "Shipment",
"Extracted order": "Extracted order",
"Pending order": "Pending order",
"Temperature": "Temperature",
"Humidity": "Humidity",
"Pending storage": "Pending storage",
"Total storage": "Total storage",
"Application completion": "Application completion",
"Processed application": "Processed application",
"Pending application": "Pending application",
"pending inspection material": "pending inspection material",
"rejected": "rejected",
"accepted": "accepted",
"escalated": "escalated",
"inspected material": "inspected material",
"total material": "total material",
"stock in escalation list": "stock in escalation list",
"Responsible for handling colleagues": "Responsible for handling colleagues",
"Completed QC Total": "Completed QC Total",
"QC Fail Count": "QC Fail Count",
"DN Date": "DN Date",
"Received Qty": "Received Qty",
"Po Code": "Po Code",
"My Escalation List": "My Escalation List",
"Escalation List": "Escalation List",
"Purchase UoM": "Purchase UoM",
"QC Completed Count": "QC Completed Count",
"QC Fail-Total Count": "QC Fail-Total Count",
"escalationStatus": "escalationStatus",
"escalated datetime": "escalated datetime",
"escalateFrom": "escalateFrom",
"No": "No",
"Responsible Escalation List": "Responsible Escalation List",
"show completed logs": "show completed logs",
"Rows per page": "Rows per page",
"Truck Schedule Dashboard": "Truck Schedule Dashboard",
"Store ID": "Store ID",
"All Stores": "All Stores",
"Auto-refresh every 5 minutes": "Auto-refresh every 5 minutes",
"Last updated": "Last updated",
"Truck Schedule": "Truck Schedule",
"Time Remaining": "Time Remaining",
"No. of Shops": "No. of Shops",
"Total Items": "Total Items",
"Tickets Released": "Tickets Released",
"First Ticket Start": "First Ticket Start",
"Tickets Completed": "Tickets Completed",
"Last Ticket End": "Last Ticket End",
"Pick Time (min)": "Pick Time (min)",
"No truck schedules available for today": "No truck schedules available for today"
}

+ 1
- 0
src/i18n/zh/common.json Просмотреть файл

@@ -322,6 +322,7 @@
"ShopAndTruck": "店鋪路線管理",
"Shop Information": "店鋪資訊",
"Shop Name": "店鋪名稱",
"Shop Branch": "店鋪分店",
"Shop Code": "店鋪編號",
"Truck Lane": "卡車路線",
"Truck Lane Detail": "卡車路線詳情",


+ 16
- 1
src/i18n/zh/dashboard.json Просмотреть файл

@@ -57,5 +57,20 @@
"No": "無",
"Responsible Escalation List": "負責的上報列表",
"show completed logs": "顯示已完成上報",
"Rows per page": "每頁行數"
"Rows per page": "每頁行數",
"Truck Schedule Dashboard": "車輛調度儀表板",
"Store ID": "樓層",
"All Stores": "所有樓層",
"Auto-refresh every 5 minutes": "每5分鐘自動刷新",
"Last updated": "最後更新",
"Truck Schedule": "車輛班次",
"Time Remaining": "剩餘時間",
"No. of Shops": "門店數量",
"Total Items": "總貨品數",
"Tickets Released": "已發放成品出倉單",
"First Ticket Start": "首單開始時間",
"Tickets Completed": "已完成成品出倉單",
"Last Ticket End": "末單結束時間",
"Pick Time (min)": "揀貨時間(分鐘)",
"No truck schedules available for today": "今日無車輛調度計劃"
}

Загрузка…
Отмена
Сохранить