Просмотр исходного кода

update handle shop/ missing trucklane, update manage by shop, added manage by trucklaneCode. Applied Translation.

master
Tommy\2Fi-Staff 4 дней назад
Родитель
Сommit
9ed78c4c2f
11 измененных файлов: 1122 добавлений и 124 удалений
  1. +2
    -2
      src/app/(main)/settings/shop/detail/page.tsx
  2. +2
    -2
      src/app/(main)/settings/shop/page.tsx
  3. +16
    -0
      src/app/(main)/settings/shop/truckdetail/page.tsx
  4. +38
    -2
      src/app/api/shop/actions.ts
  5. +16
    -0
      src/app/api/shop/client.ts
  6. +4
    -1
      src/components/Breadcrumb/Breadcrumb.tsx
  7. +171
    -55
      src/components/Shop/Shop.tsx
  8. +59
    -61
      src/components/Shop/ShopDetail.tsx
  9. +269
    -0
      src/components/Shop/TruckLane.tsx
  10. +474
    -0
      src/components/Shop/TruckLaneDetail.tsx
  11. +71
    -1
      src/i18n/zh/common.json

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

@@ -4,9 +4,9 @@ import { I18nProvider, getServerI18n } from "@/i18n";
import GeneralLoading from "@/components/General/GeneralLoading";

export default async function ShopDetailPage() {
const { t } = await getServerI18n("shop");
const { t } = await getServerI18n("shop", "common");
return (
<I18nProvider namespaces={["shop"]}>
<I18nProvider namespaces={["shop", "common"]}>
<Suspense fallback={<GeneralLoading />}>
<ShopDetail />
</Suspense>


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

@@ -8,9 +8,9 @@ import { notFound } from "next/navigation";


export default async function ShopPage() {
const { t } = await getServerI18n("shop");
const { t } = await getServerI18n("shop", "common");
return (
<I18nProvider namespaces={["shop"]}>
<I18nProvider namespaces={["shop", "common"]}>
<Suspense fallback={<ShopWrapper.Loading />}>
<ShopWrapper />
</Suspense>


+ 16
- 0
src/app/(main)/settings/shop/truckdetail/page.tsx Просмотреть файл

@@ -0,0 +1,16 @@
import { Suspense } from "react";
import TruckLaneDetail from "@/components/Shop/TruckLaneDetail";
import { I18nProvider, getServerI18n } from "@/i18n";
import GeneralLoading from "@/components/General/GeneralLoading";

export default async function TruckLaneDetailPage() {
const { t } = await getServerI18n("shop", "common");
return (
<I18nProvider namespaces={["shop", "common"]}>
<Suspense fallback={<GeneralLoading />}>
<TruckLaneDetail />
</Suspense>
</I18nProvider>
);
}


+ 38
- 2
src/app/api/shop/actions.ts Просмотреть файл

@@ -24,9 +24,11 @@ export interface ShopAndTruck{
contactName: String;
truckLanceCode: String;
DepartureTime: String;
LoadingSequence: number;
LoadingSequence?: number | null;
districtReference: Number;
Store_id: Number
Store_id: Number;
remark?: String | null;
truckId?: number;
}

export interface Shop{
@@ -60,6 +62,11 @@ export interface DeleteTruckLane {
id: number;
}

export interface UpdateLoadingSequenceRequest {
id: number;
loadingSequence: number;
}

export interface SaveTruckRequest {
id?: number | null;
store_id: string;
@@ -132,6 +139,35 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => {
export const createTruckAction = async (data: SaveTruckRequest) => {
const endpoint = `${BASE_API_URL}/truck/create`;
return serverFetchJson<MessageResponse>(endpoint, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const findAllUniqueTruckLaneCombinationsAction = cache(async () => {
const endpoint = `${BASE_API_URL}/truck/findAllUniqueTruckLanceCodeAndRemarkCombinations`;
return serverFetchJson<Truck[]>(endpoint, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
});

export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLanceCode: string, remark: string) => {
const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndRemarkAndDeletedFalse`;
const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}&remark=${encodeURIComponent(remark)}`;
return serverFetchJson<ShopAndTruck[]>(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
});

export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => {
const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`;
return serverFetchJson<MessageResponse>(endpoint, {
method: "POST",
body: JSON.stringify(data),


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

@@ -6,9 +6,13 @@ import {
updateTruckLaneAction,
deleteTruckLaneAction,
createTruckAction,
findAllUniqueTruckLaneCombinationsAction,
findAllShopsByTruckLanceCodeAndRemarkAction,
updateLoadingSequenceAction,
type SaveTruckLane,
type DeleteTruckLane,
type SaveTruckRequest,
type UpdateLoadingSequenceRequest,
type MessageResponse
} from "./actions";

@@ -32,4 +36,16 @@ export const createTruckClient = async (data: SaveTruckRequest): Promise<Message
return await createTruckAction(data);
};

export const findAllUniqueTruckLaneCombinationsClient = async () => {
return await findAllUniqueTruckLaneCombinationsAction();
};

export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode: string, remark: string) => {
return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark);
};

export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise<MessageResponse> => {
return await updateLoadingSequenceAction(data);
};

export default fetchAllShopsClient;

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

@@ -16,7 +16,10 @@ const pathToLabelMap: { [path: string]: string } = {
"/settings/qcItem": "Qc Item",
"/settings/qrCodeHandle": "QR Code Handle",
"/settings/rss": "Demand Forecast Setting",
"/settings/equipment": "Equipment",
"/settings/equipment": "Equipment",
"/settings/shop": "Shop",
"/settings/shop/detail": "Shop Detail",
"/settings/shop/truckdetail": "Truck Lane Detail",
"/scheduling/rough": "Demand Forecast",
"/scheduling/rough/edit": "FG & Material Demand Forecast Detail",
"/scheduling/detailed": "Detail Scheduling",


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

@@ -10,14 +10,22 @@ import {
Alert,
CircularProgress,
Chip,
Tabs,
Tab,
Select,
MenuItem,
FormControl,
InputLabel,
} from "@mui/material";
import { useState, useMemo, useCallback, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import SearchBox, { Criterion } from "../SearchBox";
import SearchResults, { Column } from "../SearchResults";
import { defaultPagingController } from "../SearchResults/SearchResults";
import { fetchAllShopsClient } from "@/app/api/shop/client";
import type { Shop, ShopAndTruck } from "@/app/api/shop/actions";
import TruckLane from "./TruckLane";

type ShopRow = Shop & {
actions?: string;
@@ -33,17 +41,20 @@ type SearchQuery = {
type SearchParamNames = keyof SearchQuery;

const Shop: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
const [activeTab, setActiveTab] = useState<number>(0);
const [rows, setRows] = useState<ShopRow[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [filters, setFilters] = useState<Record<string, string>>({});
const [statusFilter, setStatusFilter] = useState<string>("all");
const [pagingController, setPagingController] = useState(defaultPagingController);

// client-side filtered rows (contains-matching)
// client-side filtered rows (contains-matching + status filter)
const filteredRows = useMemo(() => {
const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== "");
const normalized = (rows || []).filter((r) => {
let normalized = (rows || []).filter((r) => {
// apply contains matching for each active filter
for (const k of fKeys) {
const v = String((filters as any)[k] ?? "").trim();
@@ -63,8 +74,16 @@ const Shop: React.FC = () => {
}
return true;
});
// Apply status filter
if (statusFilter !== "all") {
normalized = normalized.filter((r) => {
return r.truckLanceStatus === statusFilter;
});
}
return normalized;
}, [rows, filters]);
}, [rows, filters, statusFilter]);

// Check if a shop has missing truckLanceCode data
const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => {
@@ -72,16 +91,73 @@ const Shop: React.FC = () => {
return "no-truck";
}

// Check if shop has any actual truck lanes (not just null entries from LEFT JOIN)
// A shop with no trucks will have entries with null truckLanceCode
const hasAnyTruckLane = shopTrucks.some((truck) => {
const truckLanceCode = (truck as any).truckLanceCode;
return truckLanceCode != null && String(truckLanceCode).trim() !== "";
});

if (!hasAnyTruckLane) {
return "no-truck";
}

// Check each truckLanceCode entry for missing data
for (const truck of shopTrucks) {
const hasTruckLanceCode = truck.truckLanceCode && String(truck.truckLanceCode).trim() !== "";
const hasDepartureTime = truck.DepartureTime && String(truck.DepartureTime).trim() !== "";
const hasLoadingSequence = truck.LoadingSequence !== null && truck.LoadingSequence !== undefined;
const hasDistrictReference = truck.districtReference !== null && truck.districtReference !== undefined;
const hasStoreId = truck.Store_id !== null && truck.Store_id !== undefined;
// Skip entries without truckLanceCode (they're from LEFT JOIN when no trucks exist)
const truckLanceCode = (truck as any).truckLanceCode;
if (!truckLanceCode || String(truckLanceCode).trim() === "") {
continue; // Skip this entry, it's not a real truck lane
}
// Check truckLanceCode: must exist and not be empty (already validated above)
const hasTruckLanceCode = truckLanceCode != null && String(truckLanceCode).trim() !== "";
// Check departureTime: must exist and not be empty
// Can be array format [hours, minutes] or string format
const departureTime = (truck as any).departureTime || (truck as any).DepartureTime;
let hasDepartureTime = false;
if (departureTime != null) {
if (Array.isArray(departureTime) && departureTime.length >= 2) {
// Array format [hours, minutes]
hasDepartureTime = true;
} else {
// String format
const timeStr = String(departureTime).trim();
hasDepartureTime = timeStr !== "" && timeStr !== "-";
}
}
// Check loadingSequence: must exist and not be 0
const loadingSeq = (truck as any).loadingSequence || (truck as any).LoadingSequence;
const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null;
const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0;
// Check districtReference: must exist and not be 0
const districtRef = (truck as any).districtReference;
const districtRefNum = districtRef != null && districtRef !== undefined ? Number(districtRef) : null;
const hasDistrictReference = districtRefNum !== null && !isNaN(districtRefNum) && districtRefNum !== 0;
// Check storeId: must exist and not be 0 (can be string "2F"/"4F" or number)
// Actual field name in JSON is store_id (underscore, lowercase)
const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id;
let storeIdValid = false;
if (storeId != null && storeId !== undefined && storeId !== "") {
const storeIdStr = String(storeId).trim();
// If it's "2F" or "4F", it's valid (not 0)
if (storeIdStr === "2F" || storeIdStr === "4F") {
storeIdValid = true;
} else {
const storeIdNum = Number(storeId);
// If it's a valid number and not 0, it's valid
if (!isNaN(storeIdNum) && storeIdNum !== 0) {
storeIdValid = true;
}
}
}

// If any required field is missing, return "missing"
if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !hasStoreId) {
// If any required field is missing or equals 0, return "missing"
if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !storeIdValid) {
return "missing";
}
}
@@ -149,50 +225,50 @@ const Shop: React.FC = () => {
);

const criteria: Criterion<SearchParamNames>[] = [
{ type: "text", label: "id", paramName: "id" },
{ type: "text", label: "code", paramName: "code" },
{ type: "text", label: "name", paramName: "name" },
{ type: "text", label: t("id"), paramName: "id" },
{ type: "text", label: t("code"), paramName: "code" },
{ type: "text", label: t("name"), paramName: "name" },
];

const columns: Column<ShopRow>[] = [
{
name: "id",
label: "Id",
label: t("id"),
type: "integer",
renderCell: (item) => String(item.id ?? ""),
},
{
name: "code",
label: "Code",
label: t("Code"),
renderCell: (item) => String(item.code ?? ""),
},
{
name: "name",
label: "Name",
label: t("Name"),
renderCell: (item) => String(item.name ?? ""),
},
{
name: "addr3",
label: "Addr3",
label: t("Addr3"),
renderCell: (item) => String((item as any).addr3 ?? ""),
},
{
name: "truckLanceStatus",
label: "TruckLance Status",
label: t("TruckLance Status"),
renderCell: (item) => {
const status = item.truckLanceStatus;
if (status === "complete") {
return <Chip label="Complete" color="success" size="small" />;
return <Chip label={t("Complete")} color="success" size="small" />;
} else if (status === "missing") {
return <Chip label="Missing Data" color="warning" size="small" />;
return <Chip label={t("Missing Data")} color="warning" size="small" />;
} else {
return <Chip label="No TruckLance" color="error" size="small" />;
return <Chip label={t("No TruckLance")} color="error" size="small" />;
}
},
},
{
name: "actions",
label: "Actions",
label: t("Actions"),
headerAlign: "right",
renderCell: (item) => (
<Button
@@ -200,56 +276,96 @@ const Shop: React.FC = () => {
variant="outlined"
onClick={() => handleViewDetail(item)}
>
View Detail
{t("View Detail")}
</Button>
),
},
];

useEffect(() => {
fetchAllShops();
}, []);
if (activeTab === 0) {
fetchAllShops();
}
}, [activeTab]);

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};

return (
<Box>
<Card sx={{ mb: 2 }}>
<CardContent>
<SearchBox
criteria={criteria as Criterion<string>[]}
onSearch={handleSearch}
onReset={() => {
setRows([]);
setFilters({});
<Tabs
value={activeTab}
onChange={handleTabChange}
sx={{
mb: 3,
borderBottom: 1,
borderColor: 'divider'
}}
/>
</CardContent>
</Card>

<Card>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
<Typography variant="h6">Shop</Typography>
</Stack>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
>
<Tab label={t("Shop")} />
<Tab label={t("Truck Lane")} />
</Tabs>

{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
) : (
<SearchResults
items={filteredRows}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
{activeTab === 0 && (
<SearchBox
criteria={criteria as Criterion<string>[]}
onSearch={handleSearch}
onReset={() => {
setRows([]);
setFilters({});
}}
/>
)}
</CardContent>
</Card>

{activeTab === 0 && (
<Card>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
<Typography variant="h6">{t("Shop")}</Typography>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>{t("Filter by Status")}</InputLabel>
<Select
value={statusFilter}
label={t("Filter by Status")}
onChange={(e) => setStatusFilter(e.target.value)}
>
<MenuItem value="all">{t("All")}</MenuItem>
<MenuItem value="complete">{t("Complete")}</MenuItem>
<MenuItem value="missing">{t("Missing Data")}</MenuItem>
<MenuItem value="no-truck">{t("No TruckLance")}</MenuItem>
</Select>
</FormControl>
</Stack>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}

{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
) : (
<SearchResults
items={filteredRows}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
/>
)}
</CardContent>
</Card>
)}

{activeTab === 1 && (
<TruckLane />
)}
</Box>
);
};


+ 59
- 61
src/components/Shop/ShopDetail.tsx Просмотреть файл

@@ -38,6 +38,7 @@ import AddIcon from "@mui/icons-material/Add";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useTranslation } from "react-i18next";
import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions";
import {
fetchAllShopsClient,
@@ -131,6 +132,7 @@ const parseDepartureTimeForBackend = (time: string): string => {
};

const ShopDetail: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
const searchParams = useSearchParams();
const shopId = searchParams.get("id");
@@ -163,14 +165,14 @@ const ShopDetail: React.FC = () => {

// If session is unauthenticated, don't make API calls (middleware will handle redirect)
if (sessionStatus === "unauthenticated" || !session) {
setError("Please log in to view shop details");
setError(t("Please log in to view shop details"));
setLoading(false);
return;
}

const fetchShopDetail = async () => {
if (!shopId) {
setError("Shop ID is required");
setError(t("Shop ID is required"));
setLoading(false);
return;
}
@@ -178,7 +180,7 @@ const ShopDetail: React.FC = () => {
// Convert shopId to number for proper filtering
const shopIdNum = parseInt(shopId, 10);
if (isNaN(shopIdNum)) {
setError("Invalid Shop ID");
setError(t("Invalid Shop ID"));
setLoading(false);
return;
}
@@ -212,7 +214,7 @@ const ShopDetail: React.FC = () => {
contactName: shopData.contactName ?? "",
});
} else {
setError("Shop not found");
setError(t("Shop not found"));
setLoading(false);
return;
}
@@ -233,7 +235,7 @@ const ShopDetail: React.FC = () => {
} catch (err: any) {
console.error("Failed to load shop detail:", err);
// Handle errors gracefully - don't trigger auto-logout
const errorMessage = err?.message ?? String(err) ?? "Failed to load shop details";
const errorMessage = err?.message ?? String(err) ?? t("Failed to load shop details");
setError(errorMessage);
} finally {
setLoading(false);
@@ -273,13 +275,13 @@ const ShopDetail: React.FC = () => {

const handleSave = async (index: number) => {
if (!shopId) {
setError("Shop ID is required");
setError(t("Shop ID is required"));
return;
}

const truck = editedTruckData[index];
if (!truck || !truck.id) {
setError("Invalid truck data");
setError(t("Invalid shop data"));
return;
}

@@ -335,7 +337,7 @@ const ShopDetail: React.FC = () => {
setUniqueRemarks(remarks);
} catch (err: any) {
console.error("Failed to save truck data:", err);
setError(err?.message ?? String(err) ?? "Failed to save truck data");
setError(err?.message ?? String(err) ?? t("Failed to save truck data"));
} finally {
setSaving(false);
}
@@ -351,12 +353,12 @@ const ShopDetail: React.FC = () => {
};

const handleDelete = async (truckId: number) => {
if (!window.confirm("Are you sure you want to delete this truck lane?")) {
if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) {
return;
}

if (!shopId) {
setError("Shop ID is required");
setError(t("Shop ID is required"));
return;
}

@@ -373,7 +375,7 @@ const ShopDetail: React.FC = () => {
setEditingRowIndex(null);
} catch (err: any) {
console.error("Failed to delete truck lane:", err);
setError(err?.message ?? String(err) ?? "Failed to delete truck lane");
setError(err?.message ?? String(err) ?? t("Failed to delete truck lane"));
} finally {
setSaving(false);
}
@@ -409,19 +411,19 @@ const ShopDetail: React.FC = () => {
const missingFields: string[] = [];

if (!shopId || !shopDetail) {
missingFields.push("Shop information");
missingFields.push(t("Shop Information"));
}

if (!newTruck.truckLanceCode.trim()) {
missingFields.push("TruckLance Code");
missingFields.push(t("TruckLance Code"));
}

if (!newTruck.departureTime) {
missingFields.push("Departure Time");
missingFields.push(t("Departure Time"));
}

if (missingFields.length > 0) {
const message = `Please fill in the following required fields: ${missingFields.join(", ")}`;
const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`;
setSnackbarMessage(message);
setSnackbarOpen(true);
return;
@@ -461,7 +463,7 @@ const ShopDetail: React.FC = () => {
handleCloseAddDialog();
} catch (err: any) {
console.error("Failed to create truck:", err);
setError(err?.message ?? String(err) ?? "Failed to create truck");
setError(err?.message ?? String(err) ?? t("Failed to create truck"));
} finally {
setSaving(false);
}
@@ -475,12 +477,12 @@ const ShopDetail: React.FC = () => {
);
}
if (error) {
return (
return (
<Box>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
<Button onClick={() => router.back()}>Go Back</Button>
<Button onClick={() => router.back()}>{t("Back")}</Button>
</Box>
);
}
@@ -489,9 +491,9 @@ const ShopDetail: React.FC = () => {
return (
<Box>
<Alert severity="warning" sx={{ mb: 2 }}>
Shop not found
{t("Shop not found")}
</Alert>
<Button onClick={() => router.back()}>Go Back</Button>
<Button onClick={() => router.back()}>{t("Back")}</Button>
</Box>
);
}
@@ -501,49 +503,45 @@ const ShopDetail: React.FC = () => {
<Card sx={{ mb: 2 }}>
<CardContent>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
<Typography variant="h6">Shop Information</Typography>
<Button onClick={() => router.back()}>Back</Button>
<Typography variant="h6">{t("Shop Information")}</Typography>
<Button onClick={() => router.back()}>{t("Back")}</Button>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Box>
<Typography variant="subtitle2" color="text.secondary" fontWeight="bold">Shop ID</Typography>
<Typography variant="subtitle2" color="text.secondary" fontWeight="bold">{t("Shop ID")}</Typography>
<Typography variant="body1" fontWeight="medium">{shopDetail.id}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Name</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Name")}</Typography>
<Typography variant="body1">{shopDetail.name}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Code</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Code")}</Typography>
<Typography variant="body1">{shopDetail.code}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Addr1</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Addr1")}</Typography>
<Typography variant="body1">{shopDetail.addr1 || "-"}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Addr2</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Addr2")}</Typography>
<Typography variant="body1">{shopDetail.addr2 || "-"}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Addr3</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Addr3")}</Typography>
<Typography variant="body1">{shopDetail.addr3 || "-"}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Contact No</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Contact No")}</Typography>
<Typography variant="body1">{shopDetail.contactNo || "-"}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Type</Typography>
<Typography variant="body1">{shopDetail.type || "-"}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Contact Email</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Contact Email")}</Typography>
<Typography variant="body1">{shopDetail.contactEmail || "-"}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Contact Name</Typography>
<Typography variant="subtitle2" color="text.secondary">{t("Contact Name")}</Typography>
<Typography variant="body1">{shopDetail.contactName || "-"}</Typography>
</Box>
</Box>
@@ -553,27 +551,27 @@ const ShopDetail: React.FC = () => {
<Card>
<CardContent>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
<Typography variant="h6">Truck Information</Typography>
<Typography variant="h6">{t("Truck Information")}</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleOpenAddDialog}
disabled={editingRowIndex !== null || saving}
>
Add Truck Lane
{t("Add Truck Lane")}
</Button>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>TruckLance Code</TableCell>
<TableCell>Departure Time</TableCell>
<TableCell>Loading Sequence</TableCell>
<TableCell>District Reference</TableCell>
<TableCell>Store ID</TableCell>
<TableCell>Remark</TableCell>
<TableCell>Actions</TableCell>
<TableCell>{t("TruckLance Code")}</TableCell>
<TableCell>{t("Departure Time")}</TableCell>
<TableCell>{t("Loading Sequence")}</TableCell>
<TableCell>{t("District Reference")}</TableCell>
<TableCell>{t("Store ID")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -581,7 +579,7 @@ const ShopDetail: React.FC = () => {
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
No Truck data available
{t("No Truck data available")}
</Typography>
</TableCell>
</TableRow>
@@ -725,7 +723,7 @@ const ShopDetail: React.FC = () => {
<TextField
{...params}
fullWidth
placeholder={isEditable ? "Enter or select remark" : "Not editable for this Store ID"}
placeholder={isEditable ? t("Enter or select remark") : t("Not editable for this Store ID")}
disabled={!isEditable}
/>
)}
@@ -745,7 +743,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleSave(index)}
disabled={saving}
title="Save changes"
title={t("Save changes")}
>
<SaveIcon />
</IconButton>
@@ -754,7 +752,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleCancel(index)}
disabled={saving}
title="Cancel editing"
title={t("Cancel editing")}
>
<CancelIcon />
</IconButton>
@@ -766,7 +764,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleEdit(index)}
disabled={editingRowIndex !== null}
title="Edit truck lane"
title={t("Edit truck lane")}
>
<EditIcon />
</IconButton>
@@ -776,7 +774,7 @@ const ShopDetail: React.FC = () => {
size="small"
onClick={() => handleDelete(truck.id!)}
disabled={saving || editingRowIndex !== null}
title="Delete truck lane"
title={t("Delete truck lane")}
>
<DeleteIcon />
</IconButton>
@@ -797,13 +795,13 @@ const ShopDetail: React.FC = () => {

{/* Add Truck Dialog */}
<Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth>
<DialogTitle>Add New Truck Lane</DialogTitle>
<DialogTitle>{t("Add New Truck Lane")}</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
label="TruckLance Code"
label={t("TruckLance Code")}
fullWidth
required
value={newTruck.truckLanceCode}
@@ -813,7 +811,7 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={12}>
<TextField
label="Departure Time"
label={t("Departure Time")}
type="time"
fullWidth
required
@@ -830,7 +828,7 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={6}>
<TextField
label="Loading Sequence"
label={t("Loading Sequence")}
type="number"
fullWidth
value={newTruck.loadingSequence}
@@ -840,7 +838,7 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={6}>
<TextField
label="District Reference"
label={t("District Reference")}
type="number"
fullWidth
value={newTruck.districtReference}
@@ -850,10 +848,10 @@ const ShopDetail: React.FC = () => {
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Store ID</InputLabel>
<InputLabel>{t("Store ID")}</InputLabel>
<Select
value={newTruck.storeId}
label="Store ID"
label={t("Store ID")}
onChange={(e) => {
const newStoreId = e.target.value;
setNewTruck({
@@ -884,9 +882,9 @@ const ShopDetail: React.FC = () => {
renderInput={(params) => (
<TextField
{...params}
label="Remark"
label={t("Remark")}
fullWidth
placeholder="Enter or select remark"
placeholder={t("Enter or select remark")}
disabled={saving}
/>
)}
@@ -898,7 +896,7 @@ const ShopDetail: React.FC = () => {
</DialogContent>
<DialogActions>
<Button onClick={handleCloseAddDialog} disabled={saving}>
Cancel
{t("Cancel")}
</Button>
<Button
onClick={handleCreateTruck}
@@ -906,7 +904,7 @@ const ShopDetail: React.FC = () => {
startIcon={<SaveIcon />}
disabled={saving}
>
{saving ? "Saving..." : "Save"}
{saving ? t("Submitting...") : t("Save")}
</Button>
</DialogActions>
</Dialog>


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

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

import {
Box,
Card,
CardContent,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Paper,
Button,
CircularProgress,
Alert,
} from "@mui/material";
import { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { findAllUniqueTruckLaneCombinationsClient } from "@/app/api/shop/client";
import type { Truck } from "@/app/api/shop/actions";
import SearchBox, { Criterion } from "../SearchBox";

// Utility function to format departureTime to HH:mm format
const formatDepartureTime = (time: string | number[] | null | undefined): string => {
if (!time) return "-";
// Handle array format [hours, minutes] from API
if (Array.isArray(time) && time.length >= 2) {
const hours = time[0];
const minutes = time[1];
if (typeof hours === 'number' && typeof minutes === 'number' &&
hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
}
}
const timeStr = String(time).trim();
if (!timeStr || timeStr === "-") return "-";
// If already in HH:mm format, return as is
if (/^\d{1,2}:\d{2}$/.test(timeStr)) {
const [hours, minutes] = timeStr.split(":");
return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
}
return timeStr;
};

type SearchQuery = {
truckLanceCode: string;
departureTime: string;
storeId: string;
};

type SearchParamNames = keyof SearchQuery;

const TruckLane: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
const [truckData, setTruckData] = useState<Truck[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [filters, setFilters] = useState<Record<string, string>>({});
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);

useEffect(() => {
const fetchTruckLanes = async () => {
setLoading(true);
setError(null);
try {
const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
setTruckData(data || []);
} 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();
}, []);

// Client-side filtered rows (contains-matching)
const filteredRows = useMemo(() => {
const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== "");
const normalized = (truckData || []).filter((r) => {
// Apply contains matching for each active filter
for (const k of fKeys) {
const v = String((filters as any)[k] ?? "").trim();
if (k === "truckLanceCode") {
const rv = String((r as any).truckLanceCode ?? "").trim();
if (!rv.toLowerCase().includes(v.toLowerCase())) return false;
} else if (k === "departureTime") {
const formattedTime = formatDepartureTime(
Array.isArray(r.departureTime)
? r.departureTime
: (r.departureTime ? String(r.departureTime) : null)
);
if (!formattedTime.toLowerCase().includes(v.toLowerCase())) return false;
} else if (k === "storeId") {
const rv = String((r as any).storeId ?? "").trim();
const storeIdStr = typeof rv === 'string' ? rv : String(rv);
// Convert numeric values to display format for comparison
let displayStoreId = storeIdStr;
if (storeIdStr === "2" || storeIdStr === "2F") displayStoreId = "2F";
if (storeIdStr === "4" || storeIdStr === "4F") displayStoreId = "4F";
if (!displayStoreId.toLowerCase().includes(v.toLowerCase())) return false;
}
}
return true;
});
return normalized;
}, [truckData, filters]);

// Paginated rows
const paginatedRows = useMemo(() => {
const startIndex = page * rowsPerPage;
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 handlePageChange = (event: unknown, newPage: number) => {
setPage(newPage);
};

const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0); // Reset to first page when changing rows per page
};

const handleViewDetail = (truck: Truck) => {
// Navigate to truck lane detail page
if (truck.id) {
router.push(`/settings/shop/truckdetail?id=${truck.id}`);
}
};

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: "text", label: t("Departure Time"), paramName: "departureTime" },
{ type: "text", label: t("Store ID"), paramName: "storeId" },
];

return (
<Box>
<Card sx={{ mb: 2 }}>
<CardContent>
<SearchBox
criteria={criteria as Criterion<string>[]}
onSearch={handleSearch}
onReset={() => {
setFilters({});
}}
/>
</CardContent>
</Card>

<Card>
<CardContent>
<Typography variant="h6" sx={{ mb: 2 }}>{t("Truck Lane")}</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("TruckLance Code")}</TableCell>
<TableCell>{t("Departure Time")}</TableCell>
<TableCell>{t("Store ID")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedRows.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
<Typography variant="body2" color="text.secondary">
{t("No Truck Lane data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedRows.map((truck, index) => {
const storeId = truck.storeId;
const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "-";
const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F"
: storeIdStr === "4" || storeIdStr === "4F" ? "4F"
: storeIdStr;
return (
<TableRow key={truck.id ?? `truck-${index}`}>
<TableCell>
{String(truck.truckLanceCode || "-")}
</TableCell>
<TableCell>
{formatDepartureTime(
Array.isArray(truck.departureTime)
? truck.departureTime
: (truck.departureTime ? String(truck.departureTime) : null)
)}
</TableCell>
<TableCell>
{displayStoreId}
</TableCell>
<TableCell>
{String(truck.remark || "-")}
</TableCell>
<TableCell align="right">
<Button
size="small"
variant="outlined"
onClick={() => handleViewDetail(truck)}
>
{t("View Detail")}
</Button>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<TablePagination
component="div"
count={filteredRows.length}
page={page}
onPageChange={handlePageChange}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleRowsPerPageChange}
rowsPerPageOptions={[5, 10, 25, 50]}
/>
</TableContainer>
</CardContent>
</Card>
</Box>
);
};

export default TruckLane;


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

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

import {
Box,
Card,
CardContent,
Typography,
Button,
CircularProgress,
Alert,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
IconButton,
Snackbar,
TextField,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Cancel";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeAndRemarkClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client";
import type { Truck, ShopAndTruck } from "@/app/api/shop/actions";

// Utility function to format departureTime to HH:mm format
const formatDepartureTime = (time: string | number[] | null | undefined): string => {
if (!time) return "-";
// Handle array format [hours, minutes] from API
if (Array.isArray(time) && time.length >= 2) {
const hours = time[0];
const minutes = time[1];
if (typeof hours === 'number' && typeof minutes === 'number' &&
hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
}
}
const timeStr = String(time).trim();
if (!timeStr || timeStr === "-") return "-";
// If already in HH:mm format, return as is
if (/^\d{1,2}:\d{2}$/.test(timeStr)) {
const [hours, minutes] = timeStr.split(":");
return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
}
return timeStr;
};

const TruckLaneDetail: React.FC = () => {
const { t } = useTranslation("common");
const router = useRouter();
const searchParams = useSearchParams();
const truckId = searchParams.get("id");
const [truckData, setTruckData] = useState<Truck | null>(null);
const [shopsData, setShopsData] = useState<ShopAndTruck[]>([]);
const [editedShopsData, setEditedShopsData] = useState<ShopAndTruck[]>([]);
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [shopsLoading, setShopsLoading] = useState<boolean>(false);
const [saving, setSaving] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({
open: false,
message: "",
severity: "success",
});

useEffect(() => {
const fetchTruckLaneDetail = async () => {
if (!truckId) {
setError(t("Truck ID is required"));
setLoading(false);
return;
}

setLoading(true);
setError(null);
try {
const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
const truck = data.find((t) => t.id?.toString() === truckId);
if (truck) {
setTruckData(truck);
// Fetch shops using this truck lane
await fetchShopsByTruckLane(String(truck.truckLanceCode || ""), String(truck.remark || ""));
} else {
setError(t("Truck lane not found"));
}
} catch (err: any) {
console.error("Failed to load truck lane detail:", err);
setError(err?.message ?? String(err) ?? t("Failed to load truck lane detail"));
} finally {
setLoading(false);
}
};

fetchTruckLaneDetail();
}, [truckId]);

const fetchShopsByTruckLane = async (truckLanceCode: string, remark: string) => {
setShopsLoading(true);
try {
const shops = await findAllShopsByTruckLanceCodeAndRemarkClient(truckLanceCode, remark || "");
setShopsData(shops || []);
setEditedShopsData(shops || []);
} catch (err: any) {
console.error("Failed to load shops:", err);
setSnackbar({
open: true,
message: err?.message ?? String(err) ?? t("Failed to load shops"),
severity: "error",
});
} finally {
setShopsLoading(false);
}
};

const handleEdit = (index: number) => {
setEditingRowIndex(index);
const updated = [...shopsData];
updated[index] = { ...updated[index] };
setEditedShopsData(updated);
};

const handleCancel = (index: number) => {
setEditingRowIndex(null);
setEditedShopsData([...shopsData]);
};

const handleSave = async (index: number) => {
const shop = editedShopsData[index];
if (!shop || !shop.truckId) {
setSnackbar({
open: true,
message: t("Invalid shop data"),
severity: "error",
});
return;
}

setSaving(true);
setError(null);
try {
// Get LoadingSequence from edited data - handle both PascalCase and camelCase
const editedShop = editedShopsData[index];
const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence;
const loadingSequenceValue = (loadingSeq !== null && loadingSeq !== undefined) ? Number(loadingSeq) : 0;

if (!shop.truckId) {
setSnackbar({
open: true,
message: "Truck ID is required",
severity: "error",
});
return;
}

await updateLoadingSequenceClient({
id: shop.truckId,
loadingSequence: loadingSequenceValue,
});

setSnackbar({
open: true,
message: t("Loading sequence updated successfully"),
severity: "success",
});

// Refresh the shops list
if (truckData) {
await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || ""));
}
setEditingRowIndex(null);
} catch (err: any) {
console.error("Failed to save loading sequence:", err);
setSnackbar({
open: true,
message: err?.message ?? String(err) ?? t("Failed to save loading sequence"),
severity: "error",
});
} finally {
setSaving(false);
}
};

const handleLoadingSequenceChange = (index: number, value: string) => {
const updated = [...editedShopsData];
const numValue = parseInt(value, 10);
updated[index] = {
...updated[index],
LoadingSequence: isNaN(numValue) ? 0 : numValue,
};
setEditedShopsData(updated);
};

const handleDelete = async (truckIdToDelete: number) => {
if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) {
return;
}

try {
await deleteTruckLaneClient({ id: truckIdToDelete });
setSnackbar({
open: true,
message: t("Truck lane deleted successfully"),
severity: "success",
});
// Refresh the shops list
if (truckData) {
await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || ""));
}
} catch (err: any) {
console.error("Failed to delete truck lane:", err);
setSnackbar({
open: true,
message: err?.message ?? String(err) ?? t("Failed to delete truck lane"),
severity: "error",
});
}
};

const handleBack = () => {
router.push("/settings/shop");
};

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>
<Button variant="contained" onClick={handleBack}>
{t("Back to Truck Lane List")}
</Button>
</Box>
);
}

if (!truckData) {
return (
<Box>
<Alert severity="warning" sx={{ mb: 2 }}>
{t("No truck lane data available")}
</Alert>
<Button variant="contained" onClick={handleBack}>
{t("Back to Truck Lane List")}
</Button>
</Box>
);
}

const storeId = truckData.storeId;
const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "-";
const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F"
: storeIdStr === "4" || storeIdStr === "4F" ? "4F"
: storeIdStr;

return (
<Box>
<Card sx={{ mb: 2 }}>
<CardContent>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
<Typography variant="h5">{t("Truck Lane Detail")}</Typography>
<Button variant="outlined" onClick={handleBack}>
{t("Back to Truck Lane List")}
</Button>
</Box>
</CardContent>
</Card>

<Card>
<CardContent>
<Paper sx={{ p: 3 }}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
{t("TruckLance Code")}
</Typography>
<Typography variant="body1" sx={{ mt: 1 }}>
{String(truckData.truckLanceCode || "-")}
</Typography>
</Grid>

<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
{t("Departure Time")}
</Typography>
<Typography variant="body1" sx={{ mt: 1 }}>
{formatDepartureTime(
Array.isArray(truckData.departureTime)
? truckData.departureTime
: (truckData.departureTime ? String(truckData.departureTime) : null)
)}
</Typography>
</Grid>

<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
{t("Store ID")}
</Typography>
<Typography variant="body1" sx={{ mt: 1 }}>
{displayStoreId}
</Typography>
</Grid>

<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
{t("Remark")}
</Typography>
<Typography variant="body1" sx={{ mt: 1 }}>
{String(truckData.remark || "-")}
</Typography>
</Grid>
</Grid>
</Paper>
</CardContent>
</Card>

<Card sx={{ mt: 2 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Shops Using This Truck Lane")}
</Typography>
{shopsLoading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Shop Name")}</TableCell>
<TableCell>{t("Shop Code")}</TableCell>
<TableCell>{t("Loading Sequence")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{shopsData.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
<Typography variant="body2" color="text.secondary">
{t("No shops found using this truck lane")}
</Typography>
</TableCell>
</TableRow>
) : (
shopsData.map((shop, index) => (
<TableRow key={shop.id ?? `shop-${index}`}>
<TableCell>
{String(shop.name || "-")}
</TableCell>
<TableCell>
{String(shop.code || "-")}
</TableCell>
<TableCell>
{editingRowIndex === index ? (
<TextField
size="small"
type="number"
value={(() => {
const editedShop = editedShopsData[index];
return (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence ?? 0;
})()}
onChange={(e) => handleLoadingSequenceChange(index, e.target.value)}
disabled={saving}
sx={{ width: 100 }}
/>
) : (
(() => {
// Handle both PascalCase and camelCase, and check for 0 as valid value
const loadingSeq = (shop as any).LoadingSequence ?? (shop as any).loadingSequence;
return (loadingSeq !== null && loadingSeq !== undefined)
? String(loadingSeq)
: "-";
})()
)}
</TableCell>
<TableCell>
{String(shop.remark || "-")}
</TableCell>
<TableCell align="right">
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
{editingRowIndex === index ? (
<>
<IconButton
size="small"
color="primary"
onClick={() => handleSave(index)}
disabled={saving}
title={t("Save changes")}
>
<SaveIcon />
</IconButton>
<IconButton
size="small"
color="default"
onClick={() => handleCancel(index)}
disabled={saving}
title={t("Cancel editing")}
>
<CancelIcon />
</IconButton>
</>
) : (
<>
<IconButton
size="small"
color="primary"
onClick={() => handleEdit(index)}
title={t("Edit loading sequence")}
>
<EditIcon />
</IconButton>
{shop.truckId && (
<IconButton
size="small"
color="error"
onClick={() => handleDelete(shop.truckId!)}
title={t("Delete truck lane")}
>
<DeleteIcon />
</IconButton>
)}
</>
)}
</Box>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</CardContent>
</Card>

<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
message={snackbar.message}
/>
</Box>
);
};

export default TruckLaneDetail;



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

@@ -296,5 +296,75 @@
"Total lines: ": "總數量:",
"Balance": "可用數量",
"Submitting...": "提交中...",
"Batch Count": "批數"
"Batch Count": "批數",
"Shop": "店鋪",
"Shop Information": "店鋪資訊",
"Shop Name": "店鋪名稱",
"Shop Code": "店鋪編號",
"Truck Lane": "卡車路線",
"Truck Lane Detail": "卡車路線詳情",
"TruckLance Code": "卡車路線編號",
"TruckLance Status": "卡車路線狀態",
"Departure Time": "出發時間",
"Loading Sequence": "裝載順序",
"District Reference": "區域參考",
"Store ID": "店鋪ID",
"Remark": "備註",
"Actions": "操作",
"View Detail": "查看詳情",
"Back": "返回",
"Back to Truck Lane List": "返回卡車路線列表",
"Back to List": "返回列表",
"Add Truck Lane": "新增卡車路線",
"Add New Truck Lane": "新增卡車路線",
"Truck Information": "卡車資訊",
"No Truck data available": "沒有卡車資料",
"No shops found using this truck lane": "沒有找到使用此卡車路線的店鋪",
"Shops Using This Truck Lane": "使用此卡車路線的店鋪",
"Complete": "完成",
"Missing Data": "缺少資料",
"No TruckLance": "無卡車路線",
"Edit shop truck lane": "編輯店鋪卡車路線",
"Delete truck lane": "刪除卡車路線",
"Edit loading sequence": "編輯裝載順序",
"Save changes": "儲存變更",
"Cancel editing": "取消編輯",
"Edit truck lane": "編輯卡車路線",
"Truck ID is required": "需要卡車ID",
"Truck lane not found": "找不到卡車路線",
"No truck lane data available": "沒有卡車路線資料",
"Failed to load truck lanes": "載入卡車路線失敗",
"Failed to load shops": "載入店鋪失敗",
"Loading sequence updated successfully": "裝載順序更新成功",
"Failed to save loading sequence": "儲存裝載順序失敗",
"Truck lane deleted successfully": "卡車路線刪除成功",
"Failed to delete truck lane": "刪除卡車路線失敗",
"Are you sure you want to delete this truck lane?": "您確定要刪除此卡車路線嗎?",
"Invalid shop data": "無效的店鋪資料",
"Contact No": "聯絡電話",
"Contact Email": "聯絡郵箱",
"Contact Name": "聯絡人",
"Addr1": "地址1",
"Addr2": "地址2",
"Addr3": "地址3",
"Shop not found": "找不到店鋪",
"Shop ID is required": "需要店鋪ID",
"Invalid Shop ID": "無效的店鋪ID",
"Failed to load shop detail": "載入店鋪詳情失敗",
"Failed to load shop details": "載入店鋪詳情失敗",
"Failed to save truck data": "儲存卡車資料失敗",
"Failed to delete truck lane": "刪除卡車路線失敗",
"Failed to create truck": "建立卡車失敗",
"Please fill in the following required fields:": "請填寫以下必填欄位:",
"TruckLance Code": "卡車路線編號",
"Enter or select remark": "輸入或選擇備註",
"Not editable for this Store ID": "此店鋪ID不可編輯",
"No Truck Lane data available": "沒有卡車路線資料",
"Please log in to view shop details": "請登入以查看店鋪詳情",
"Invalid truck data": "無效的卡車資料",
"Failed to load truck lane detail": "載入卡車路線詳情失敗",
"Shop Detail": "店鋪詳情",
"Truck Lane Detail": "卡車路線詳情",
"Filter by Status": "按狀態篩選",
"All": "全部"
}

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