Bläddra i källkod

Add "add shop in trucklane"&"add trucklane"

master
Tommy\2Fi-Staff 1 månad sedan
förälder
incheckning
187c88d1e5
6 ändrade filer med 961 tillägg och 30 borttagningar
  1. +76
    -5
      src/app/api/shop/actions.ts
  2. +35
    -4
      src/app/api/shop/client.ts
  3. +206
    -2
      src/components/Shop/TruckLane.tsx
  4. +618
    -15
      src/components/Shop/TruckLaneDetail.tsx
  5. +13
    -3
      src/i18n/en/common.json
  6. +13
    -1
      src/i18n/zh/common.json

+ 76
- 5
src/app/api/shop/actions.ts Visa fil

@@ -46,6 +46,8 @@ export interface Truck{
districtReference: Number;
storeId: Number | String;
remark?: String | null;
shopName?: String | null;
shopCode?: String | null;
}

export interface SaveTruckLane {
@@ -62,9 +64,13 @@ export interface DeleteTruckLane {
id: number;
}

export interface UpdateLoadingSequenceRequest {
export interface UpdateTruckShopDetailsRequest {
id: number;
shopId?: number | null;
shopName: string | null;
shopCode: string | null;
loadingSequence: number;
remark?: string | null;
}

export interface SaveTruckRequest {
@@ -80,6 +86,15 @@ export interface SaveTruckRequest {
remark?: string | null;
}

export interface CreateTruckWithoutShopRequest {
store_id: string;
truckLanceCode: string;
departureTime: string;
loadingSequence?: number;
districtReference?: number | null;
remark?: string | null;
}

export interface MessageResponse {
id: number | null;
name: string | null;
@@ -137,7 +152,7 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => {
};

export const createTruckAction = async (data: SaveTruckRequest) => {
const endpoint = `${BASE_API_URL}/truck/create`;
const endpoint = `${BASE_API_URL}/truck/createTruckInShop`;
return serverFetchJson<MessageResponse>(endpoint, {
method: "POST",
@@ -175,12 +190,68 @@ export const findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: s
});
});

export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => {
const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`;
export const findAllByTruckLanceCodeAndDeletedFalseAction = cache(async (truckLanceCode: string) => {
const endpoint = `${BASE_API_URL}/truck/findAllByTruckLanceCodeAndDeletedFalse`;
const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`;
return serverFetchJson<Truck[]>(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
});

export const updateTruckShopDetailsAction = async (data: UpdateTruckShopDetailsRequest) => {
const endpoint = `${BASE_API_URL}/truck/updateTruckShopDetails`;
return serverFetchJson<MessageResponse>(endpoint, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const createTruckWithoutShopAction = async (data: CreateTruckWithoutShopRequest) => {
const endpoint = `${BASE_API_URL}/truck/createTruckWithoutShop`;
return serverFetchJson<MessageResponse>(endpoint, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};
};

export const findAllUniqueShopNamesAndCodesFromTrucksAction = cache(async () => {
const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesAndCodesFromTrucks`;
return serverFetchJson<Array<{ name: string; code: string }>>(endpoint, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
});

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

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

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

+ 35
- 4
src/app/api/shop/client.ts Visa fil

@@ -9,11 +9,18 @@ import {
findAllUniqueTruckLaneCombinationsAction,
findAllShopsByTruckLanceCodeAndRemarkAction,
findAllShopsByTruckLanceCodeAction,
updateLoadingSequenceAction,
createTruckWithoutShopAction,
updateTruckShopDetailsAction,
findAllUniqueShopNamesAndCodesFromTrucksAction,
findAllUniqueRemarksFromTrucksAction,
findAllUniqueShopCodesFromTrucksAction,
findAllUniqueShopNamesFromTrucksAction,
findAllByTruckLanceCodeAndDeletedFalseAction,
type SaveTruckLane,
type DeleteTruckLane,
type SaveTruckRequest,
type UpdateLoadingSequenceRequest,
type UpdateTruckShopDetailsRequest,
type CreateTruckWithoutShopRequest,
type MessageResponse
} from "./actions";

@@ -49,8 +56,32 @@ export const findAllShopsByTruckLanceCodeClient = async (truckLanceCode: string)
return await findAllShopsByTruckLanceCodeAction(truckLanceCode);
};

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

export const updateTruckShopDetailsClient = async (data: UpdateTruckShopDetailsRequest): Promise<MessageResponse> => {
return await updateTruckShopDetailsAction(data);
};

export const createTruckWithoutShopClient = async (data: CreateTruckWithoutShopRequest): Promise<MessageResponse> => {
return await createTruckWithoutShopAction(data);
};

export const findAllUniqueShopNamesAndCodesFromTrucksClient = async () => {
return await findAllUniqueShopNamesAndCodesFromTrucksAction();
};

export const findAllUniqueRemarksFromTrucksClient = async () => {
return await findAllUniqueRemarksFromTrucksAction();
};

export const findAllUniqueShopCodesFromTrucksClient = async () => {
return await findAllUniqueShopCodesFromTrucksAction();
};

export const findAllUniqueShopNamesFromTrucksClient = async () => {
return await findAllUniqueShopNamesFromTrucksAction();
};

export default fetchAllShopsClient;

+ 206
- 2
src/components/Shop/TruckLane.tsx Visa fil

@@ -16,11 +16,24 @@ import {
Button,
CircularProgress,
Alert,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
Snackbar,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import SaveIcon from "@mui/icons-material/Save";
import { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { findAllUniqueTruckLaneCombinationsClient } from "@/app/api/shop/client";
import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client";
import type { Truck } from "@/app/api/shop/actions";
import SearchBox, { Criterion } from "../SearchBox";

@@ -50,6 +63,20 @@ const formatDepartureTime = (time: string | number[] | null | undefined): string
return timeStr;
};

// Utility function to convert HH:mm format to the format expected by backend
const parseDepartureTimeForBackend = (time: string): string => {
if (!time) return "";
const timeStr = String(time).trim();
// If already in HH:mm format, return as is
if (/^\d{1,2}:\d{2}$/.test(timeStr)) {
return timeStr;
}
// Try to format it
return formatDepartureTime(timeStr);
};

type SearchQuery = {
truckLanceCode: string;
departureTime: string;
@@ -67,6 +94,15 @@ const TruckLane: React.FC = () => {
const [filters, setFilters] = useState<Record<string, string>>({});
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
const [newTruck, setNewTruck] = useState({
truckLanceCode: "",
departureTime: "",
storeId: "2F",
});
const [saving, setSaving] = useState<boolean>(false);
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
const [snackbarMessage, setSnackbarMessage] = useState<string>("");

useEffect(() => {
const fetchTruckLanes = async () => {
@@ -158,6 +194,78 @@ const TruckLane: React.FC = () => {
}
};

const handleOpenAddDialog = () => {
setNewTruck({
truckLanceCode: "",
departureTime: "",
storeId: "2F",
});
setAddDialogOpen(true);
setError(null);
};

const handleCloseAddDialog = () => {
setAddDialogOpen(false);
setNewTruck({
truckLanceCode: "",
departureTime: "",
storeId: "2F",
});
};

const handleCreateTruck = async () => {
// Validate all required fields
const missingFields: string[] = [];

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

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

if (missingFields.length > 0) {
const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`;
setSnackbarMessage(message);
setSnackbarOpen(true);
return;
}

setSaving(true);
setError(null);
try {
const departureTime = parseDepartureTimeForBackend(newTruck.departureTime);
await createTruckWithoutShopClient({
store_id: newTruck.storeId,
truckLanceCode: newTruck.truckLanceCode.trim(),
departureTime: departureTime,
loadingSequence: 0,
districtReference: null,
remark: null,
});
// Refresh truck data after create
const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
const uniqueCodes = new Map<string, Truck>();
(data || []).forEach((truck) => {
const code = String(truck.truckLanceCode || "").trim();
if (code && !uniqueCodes.has(code)) {
uniqueCodes.set(code, truck);
}
});
setTruckData(Array.from(uniqueCodes.values()));
handleCloseAddDialog();
} catch (err: any) {
console.error("Failed to create truck:", err);
setError(err?.message ?? String(err) ?? t("Failed to create truck"));
} finally {
setSaving(false);
}
};

if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
@@ -198,7 +306,17 @@ const TruckLane: React.FC = () => {

<Card>
<CardContent>
<Typography variant="h6" sx={{ mb: 2 }}>{t("Truck Lane")}</Typography>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
<Typography variant="h6">{t("Truck Lane")}</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleOpenAddDialog}
disabled={saving}
>
{t("Add Truck Lane")}
</Button>
</Box>
<TableContainer component={Paper}>
<Table>
@@ -269,6 +387,92 @@ const TruckLane: React.FC = () => {
</TableContainer>
</CardContent>
</Card>

{/* Add Truck Dialog */}
<Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth>
<DialogTitle>{t("Add New Truck Lane")}</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
label={t("TruckLance Code")}
fullWidth
required
value={newTruck.truckLanceCode}
onChange={(e) => setNewTruck({ ...newTruck, truckLanceCode: e.target.value })}
disabled={saving}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Departure Time")}
type="time"
fullWidth
required
value={newTruck.departureTime}
onChange={(e) => setNewTruck({ ...newTruck, departureTime: e.target.value })}
disabled={saving}
InputLabelProps={{
shrink: true,
}}
inputProps={{
step: 300, // 5 minutes
}}
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>{t("Store ID")}</InputLabel>
<Select
value={newTruck.storeId}
label={t("Store ID")}
onChange={(e) => {
setNewTruck({
...newTruck,
storeId: e.target.value
});
}}
disabled={saving}
>
<MenuItem value="2F">2F</MenuItem>
<MenuItem value="4F">4F</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseAddDialog} disabled={saving}>
{t("Cancel")}
</Button>
<Button
onClick={handleCreateTruck}
variant="contained"
startIcon={<SaveIcon />}
disabled={saving}
>
{saving ? t("Submitting...") : t("Save")}
</Button>
</DialogActions>
</Dialog>

{/* Snackbar for notifications */}
<Snackbar
open={snackbarOpen}
autoHideDuration={6000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert
onClose={() => setSnackbarOpen(false)}
severity="warning"
sx={{ width: '100%' }}
>
{snackbarMessage}
</Alert>
</Snackbar>
</Box>
);
};


+ 618
- 15
src/components/Shop/TruckLaneDetail.tsx Visa fil

@@ -19,16 +19,22 @@ import {
IconButton,
Snackbar,
TextField,
Autocomplete,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} 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 AddIcon from "@mui/icons-material/Add";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client";
import type { Truck, ShopAndTruck } from "@/app/api/shop/actions";
import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeClient, deleteTruckLaneClient, updateTruckShopDetailsClient, fetchAllShopsClient, findAllUniqueShopNamesAndCodesFromTrucksClient, findAllUniqueRemarksFromTrucksClient, findAllUniqueShopCodesFromTrucksClient, findAllUniqueShopNamesFromTrucksClient, createTruckClient, findAllByTruckLanceCodeAndDeletedFalseClient } from "@/app/api/shop/client";
import type { Truck, ShopAndTruck, Shop } from "@/app/api/shop/actions";

// Utility function to format departureTime to HH:mm format
const formatDepartureTime = (time: string | number[] | null | undefined): string => {
@@ -72,12 +78,78 @@ const TruckLaneDetail: React.FC = () => {
const [shopsLoading, setShopsLoading] = useState<boolean>(false);
const [saving, setSaving] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [allShops, setAllShops] = useState<Shop[]>([]);
const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]);
const [uniqueShopCodes, setUniqueShopCodes] = useState<string[]>([]);
const [uniqueShopNames, setUniqueShopNames] = useState<string[]>([]);
const [addShopDialogOpen, setAddShopDialogOpen] = useState<boolean>(false);
const [newShop, setNewShop] = useState({
shopName: "",
shopCode: "",
loadingSequence: 0,
remark: "",
});
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({
open: false,
message: "",
severity: "success",
});

useEffect(() => {
// Fetch unique shop names and codes from truck table
const fetchShopNamesFromTrucks = async () => {
try {
const shopData = await findAllUniqueShopNamesAndCodesFromTrucksClient() as Array<{ name: string; code: string }>;
// Convert to Shop format (id will be 0 since we don't have shop IDs from truck table)
const shopList: Shop[] = shopData.map((shop) => ({
id: 0, // No shop ID available from truck table
name: shop.name || "",
code: shop.code || "",
addr3: "",
}));
setAllShops(shopList);
} catch (err: any) {
console.error("Failed to load shop names from trucks:", err);
}
};

// Fetch unique remarks from truck table
const fetchRemarksFromTrucks = async () => {
try {
const remarks = await findAllUniqueRemarksFromTrucksClient() as string[];
setUniqueRemarks(remarks || []);
} catch (err: any) {
console.error("Failed to load remarks from trucks:", err);
}
};

// Fetch unique shop codes from truck table
const fetchShopCodesFromTrucks = async () => {
try {
const codes = await findAllUniqueShopCodesFromTrucksClient() as string[];
setUniqueShopCodes(codes || []);
} catch (err: any) {
console.error("Failed to load shop codes from trucks:", err);
}
};

// Fetch unique shop names from truck table
const fetchShopNamesFromTrucksOnly = async () => {
try {
const names = await findAllUniqueShopNamesFromTrucksClient() as string[];
setUniqueShopNames(names || []);
} catch (err: any) {
console.error("Failed to load shop names from trucks:", err);
}
};

fetchShopNamesFromTrucks();
fetchRemarksFromTrucks();
fetchShopCodesFromTrucks();
fetchShopNamesFromTrucksOnly();
}, []);

useEffect(() => {
// Wait a bit to ensure searchParams are fully available
if (!truckLanceCodeParam) {
@@ -183,28 +255,55 @@ const TruckLaneDetail: React.FC = () => {
setSaving(true);
setError(null);
try {
// Get LoadingSequence from edited data - handle both PascalCase and camelCase
// Get values from edited data
const editedShop = editedShopsData[index];
const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence;
const loadingSequenceValue = (loadingSeq !== null && loadingSeq !== undefined) ? Number(loadingSeq) : 0;
// Get shopName and shopCode from edited data
const shopNameValue = editedShop.name ? String(editedShop.name).trim() : null;
const shopCodeValue = editedShop.code ? String(editedShop.code).trim() : null;
const remarkValue = editedShop.remark ? String(editedShop.remark).trim() : null;
// Get shopId from editedShop.id (which was set when shopName or shopCode was selected)
// If not found, try to find it from shop table by shopCode
let shopIdValue: number | null = null;
if (editedShop.id && editedShop.id > 0) {
shopIdValue = editedShop.id;
} else if (shopCodeValue) {
// If shopId is 0 (from truck table), try to find it from shop table
try {
const allShopsFromShopTable = await fetchAllShopsClient() as ShopAndTruck[];
const foundShop = allShopsFromShopTable.find(s => String(s.code).trim() === shopCodeValue);
if (foundShop) {
shopIdValue = foundShop.id;
}
} catch (err) {
console.error("Failed to lookup shopId:", err);
}
}

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

await updateLoadingSequenceClient({
await updateTruckShopDetailsClient({
id: shop.truckId,
shopId: shopIdValue,
shopName: shopNameValue,
shopCode: shopCodeValue,
loadingSequence: loadingSequenceValue,
remark: remarkValue || null,
});

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

@@ -214,10 +313,10 @@ const TruckLaneDetail: React.FC = () => {
}
setEditingRowIndex(null);
} catch (err: any) {
console.error("Failed to save loading sequence:", err);
console.error("Failed to save truck shop details:", err);
setSnackbar({
open: true,
message: err?.message ?? String(err) ?? t("Failed to save loading sequence"),
message: err?.message ?? String(err) ?? t("Failed to save truck shop details"),
severity: "error",
});
} finally {
@@ -235,6 +334,53 @@ const TruckLaneDetail: React.FC = () => {
setEditedShopsData(updated);
};

const handleShopNameChange = (index: number, shop: Shop | null) => {
const updated = [...editedShopsData];
if (shop) {
updated[index] = {
...updated[index],
name: shop.name,
code: shop.code,
id: shop.id, // Store shopId for later use
};
} else {
updated[index] = {
...updated[index],
name: "",
code: "",
};
}
setEditedShopsData(updated);
};

const handleShopCodeChange = (index: number, shop: Shop | null) => {
const updated = [...editedShopsData];
if (shop) {
updated[index] = {
...updated[index],
name: shop.name,
code: shop.code,
id: shop.id, // Store shopId for later use
};
} else {
updated[index] = {
...updated[index],
name: "",
code: "",
};
}
setEditedShopsData(updated);
};

const handleRemarkChange = (index: number, value: string) => {
const updated = [...editedShopsData];
updated[index] = {
...updated[index],
remark: value,
};
setEditedShopsData(updated);
};

const handleDelete = async (truckIdToDelete: number) => {
if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) {
return;
@@ -266,6 +412,208 @@ const TruckLaneDetail: React.FC = () => {
router.push("/settings/shop");
};

const handleOpenAddShopDialog = () => {
setNewShop({
shopName: "",
shopCode: "",
loadingSequence: 0,
remark: "",
});
setAddShopDialogOpen(true);
setError(null);
};

const handleCloseAddShopDialog = () => {
setAddShopDialogOpen(false);
setNewShop({
shopName: "",
shopCode: "",
loadingSequence: 0,
remark: "",
});
};

const handleNewShopNameChange = (newValue: string | null) => {
if (newValue && typeof newValue === 'string') {
// When a name is selected, try to find matching shop code
const matchingShop = allShops.find(s => String(s.name) === newValue);
if (matchingShop) {
setNewShop({
...newShop,
shopName: newValue,
shopCode: String(matchingShop.code || ""),
});
} else {
setNewShop({
...newShop,
shopName: newValue,
});
}
} else if (newValue === null) {
setNewShop({
...newShop,
shopName: "",
});
}
};

const handleNewShopCodeChange = (newValue: string | null) => {
if (newValue && typeof newValue === 'string') {
// When a code is selected, try to find matching shop name
const matchingShop = allShops.find(s => String(s.code) === newValue);
if (matchingShop) {
setNewShop({
...newShop,
shopCode: newValue,
shopName: String(matchingShop.name || ""),
});
} else {
setNewShop({
...newShop,
shopCode: newValue,
});
}
} else if (newValue === null) {
setNewShop({
...newShop,
shopCode: "",
});
}
};

const handleCreateShop = async () => {
// Validate required fields
const missingFields: string[] = [];

if (!newShop.shopName.trim()) {
missingFields.push(t("Shop Name"));
}

if (!newShop.shopCode.trim()) {
missingFields.push(t("Shop Code"));
}

if (missingFields.length > 0) {
setSnackbar({
open: true,
message: `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`,
severity: "error",
});
return;
}

if (!truckData || !truckLanceCode) {
setSnackbar({
open: true,
message: t("Truck lane information is required"),
severity: "error",
});
return;
}

setSaving(true);
setError(null);
try {
// Get storeId from truckData
const storeId = truckData.storeId;
const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "2F";
const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F"
: storeIdStr === "4" || storeIdStr === "4F" ? "4F"
: storeIdStr;

// Get departureTime from truckData
let departureTimeStr = "";
if (truckData.departureTime) {
if (Array.isArray(truckData.departureTime)) {
const [hours, minutes] = truckData.departureTime;
departureTimeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
} else {
departureTimeStr = String(truckData.departureTime);
}
}

// Look up shopId from shop table by shopCode
let shopIdValue: number | null = null;
try {
const allShopsFromShopTable = await fetchAllShopsClient() as ShopAndTruck[];
const foundShop = allShopsFromShopTable.find(s => String(s.code).trim() === newShop.shopCode.trim());
if (foundShop) {
shopIdValue = foundShop.id;
}
} catch (err) {
console.error("Failed to lookup shopId:", err);
}

// Get remark - only if storeId is "4F"
const remarkValue = displayStoreId === "4F" ? (newShop.remark?.trim() || null) : null;

// Check if there's an "Unassign" row for this truck lane that should be replaced
let unassignTruck: Truck | null = null;
try {
const allTrucks = await findAllByTruckLanceCodeAndDeletedFalseClient(String(truckData.truckLanceCode || "")) as Truck[];
unassignTruck = allTrucks.find(t =>
String(t.shopName || "").trim() === "Unassign" &&
String(t.shopCode || "").trim() === "Unassign"
) || null;
} catch (err) {
console.error("Failed to check for Unassign truck:", err);
}

if (unassignTruck && unassignTruck.id) {
// Update the existing "Unassign" row instead of creating a new one
await updateTruckShopDetailsClient({
id: unassignTruck.id,
shopId: shopIdValue || null,
shopName: newShop.shopName.trim(),
shopCode: newShop.shopCode.trim(),
loadingSequence: newShop.loadingSequence,
remark: remarkValue,
});

setSnackbar({
open: true,
message: t("Shop added to truck lane successfully"),
severity: "success",
});
} else {
// No "Unassign" row found, create a new one
await createTruckClient({
store_id: displayStoreId,
truckLanceCode: String(truckData.truckLanceCode || ""),
departureTime: departureTimeStr,
shopId: shopIdValue || 0,
shopName: newShop.shopName.trim(),
shopCode: newShop.shopCode.trim(),
loadingSequence: newShop.loadingSequence,
remark: remarkValue,
districtReference: null,
});

setSnackbar({
open: true,
message: t("Shop added to truck lane successfully"),
severity: "success",
});
}

// Refresh the shops list
if (truckLanceCode) {
await fetchShopsByTruckLane(truckLanceCode);
}
handleCloseAddShopDialog();
} catch (err: any) {
console.error("Failed to create shop in truck lane:", err);
setSnackbar({
open: true,
message: err?.message ?? String(err) ?? t("Failed to create shop in truck lane"),
severity: "error",
});
} finally {
setSaving(false);
}
};

if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
@@ -361,9 +709,19 @@ const TruckLaneDetail: React.FC = () => {

<Card sx={{ mt: 2 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Shops Using This Truck Lane")}
</Typography>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
<Typography variant="h6">
{t("Shops Using This Truck Lane")}
</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleOpenAddShopDialog}
disabled={saving || editingRowIndex !== null}
>
{t("Add Shop")}
</Button>
</Box>
{shopsLoading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
@@ -394,13 +752,143 @@ const TruckLaneDetail: React.FC = () => {
shopsData.map((shop, index) => (
<TableRow key={shop.id ?? `shop-${index}`}>
<TableCell>
{String(shop.name || "-")}
{editingRowIndex === index ? (
<Autocomplete
freeSolo
size="small"
options={uniqueShopNames}
value={String(editedShopsData[index]?.name || "")}
onChange={(event, newValue) => {
if (newValue && typeof newValue === 'string') {
// When a name is selected, try to find matching shop code
const matchingShop = allShops.find(s => String(s.name) === newValue);
if (matchingShop) {
handleShopNameChange(index, matchingShop);
} else {
// If no matching shop found, just update the name
const updated = [...editedShopsData];
updated[index] = {
...updated[index],
name: newValue,
};
setEditedShopsData(updated);
}
} else if (newValue === null) {
handleShopNameChange(index, null);
}
}}
onInputChange={(event, newInputValue, reason) => {
if (reason === 'input') {
// Allow free text input
const updated = [...editedShopsData];
updated[index] = {
...updated[index],
name: newInputValue,
};
setEditedShopsData(updated);
}
}}
renderInput={(params) => (
<TextField
{...params}
fullWidth
disabled={saving}
placeholder={t("Search or select shop name")}
/>
)}
/>
) : (
String(shop.name || "-")
)}
</TableCell>
<TableCell>
{String(shop.code || "-")}
{editingRowIndex === index ? (
<Autocomplete
freeSolo
size="small"
options={uniqueShopCodes}
value={String(editedShopsData[index]?.code || "")}
onChange={(event, newValue) => {
if (newValue && typeof newValue === 'string') {
// When a code is selected, try to find matching shop name
const matchingShop = allShops.find(s => String(s.code) === newValue);
if (matchingShop) {
handleShopCodeChange(index, matchingShop);
} else {
// If no matching shop found, just update the code
const updated = [...editedShopsData];
updated[index] = {
...updated[index],
code: newValue,
};
setEditedShopsData(updated);
}
} else if (newValue === null) {
handleShopCodeChange(index, null);
}
}}
onInputChange={(event, newInputValue, reason) => {
if (reason === 'input') {
// Allow free text input
const updated = [...editedShopsData];
updated[index] = {
...updated[index],
code: newInputValue,
};
setEditedShopsData(updated);
}
}}
renderInput={(params) => (
<TextField
{...params}
fullWidth
disabled={saving}
placeholder={t("Search or select shop code")}
/>
)}
/>
) : (
String(shop.code || "-")
)}
</TableCell>
<TableCell>
{String(shop.remark || "-")}
{editingRowIndex === index ? (
(() => {
const storeId = truckData.storeId;
const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "";
const isEditable = storeIdStr === "4F" || storeIdStr === "4";
return (
<Autocomplete
freeSolo
size="small"
options={uniqueRemarks}
value={String(editedShopsData[index]?.remark || "")}
onChange={(event, newValue) => {
if (isEditable) {
const remarkValue = typeof newValue === 'string' ? newValue : (newValue || "");
handleRemarkChange(index, remarkValue);
}
}}
onInputChange={(event, newInputValue, reason) => {
if (isEditable && reason === 'input') {
handleRemarkChange(index, newInputValue);
}
}}
disabled={saving || !isEditable}
renderInput={(params) => (
<TextField
{...params}
fullWidth
placeholder={isEditable ? t("Search or select remark") : t("Not editable for this Store ID")}
disabled={saving || !isEditable}
/>
)}
/>
);
})()
) : (
String(shop.remark || "-")
)}
</TableCell>
<TableCell>
{editingRowIndex === index ? (
@@ -454,7 +942,7 @@ const TruckLaneDetail: React.FC = () => {
size="small"
color="primary"
onClick={() => handleEdit(index)}
title={t("Edit loading sequence")}
title={t("Edit shop details")}
>
<EditIcon />
</IconButton>
@@ -482,6 +970,121 @@ const TruckLaneDetail: React.FC = () => {
</CardContent>
</Card>

{/* Add Shop Dialog */}
<Dialog open={addShopDialogOpen} onClose={handleCloseAddShopDialog} maxWidth="sm" fullWidth>
<DialogTitle>{t("Add Shop to Truck Lane")}</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Autocomplete
freeSolo
options={uniqueShopNames}
value={newShop.shopName}
onChange={(event, newValue) => {
handleNewShopNameChange(newValue);
}}
onInputChange={(event, newInputValue, reason) => {
if (reason === 'input') {
setNewShop({ ...newShop, shopName: newInputValue });
}
}}
renderInput={(params) => (
<TextField
{...params}
label={t("Shop Name")}
fullWidth
required
disabled={saving}
placeholder={t("Search or select shop name")}
/>
)}
/>
</Grid>
<Grid item xs={12}>
<Autocomplete
freeSolo
options={uniqueShopCodes}
value={newShop.shopCode}
onChange={(event, newValue) => {
handleNewShopCodeChange(newValue);
}}
onInputChange={(event, newInputValue, reason) => {
if (reason === 'input') {
setNewShop({ ...newShop, shopCode: newInputValue });
}
}}
renderInput={(params) => (
<TextField
{...params}
label={t("Shop Code")}
fullWidth
required
disabled={saving}
placeholder={t("Search or select shop code")}
/>
)}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Loading Sequence")}
type="number"
fullWidth
value={newShop.loadingSequence}
onChange={(e) => setNewShop({ ...newShop, loadingSequence: parseInt(e.target.value) || 0 })}
disabled={saving}
/>
</Grid>
{(() => {
const storeId = truckData?.storeId;
const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "";
const isEditable = storeIdStr === "4F" || storeIdStr === "4";
return isEditable ? (
<Grid item xs={12}>
<Autocomplete
freeSolo
options={uniqueRemarks}
value={newShop.remark || ""}
onChange={(event, newValue) => {
setNewShop({ ...newShop, remark: typeof newValue === 'string' ? newValue : (newValue || "") });
}}
onInputChange={(event, newInputValue, reason) => {
if (reason === 'input') {
setNewShop({ ...newShop, remark: newInputValue });
}
}}
renderInput={(params) => (
<TextField
{...params}
label={t("Remark")}
fullWidth
disabled={saving}
placeholder={t("Search or select remark")}
/>
)}
/>
</Grid>
) : null;
})()}
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseAddShopDialog} disabled={saving}>
{t("Cancel")}
</Button>
<Button
onClick={handleCreateShop}
variant="contained"
startIcon={<SaveIcon />}
disabled={saving}
>
{saving ? t("Submitting...") : t("Save")}
</Button>
</DialogActions>
</Dialog>

<Snackbar
open={snackbar.open}
autoHideDuration={6000}


+ 13
- 3
src/i18n/en/common.json Visa fil

@@ -17,7 +17,17 @@
"No": "No",
"Equipment Name": "Equipment Name",
"Equipment Code": "Equipment Code",

"ShopAndTruck": "ShopAndTruck"

"ShopAndTruck": "ShopAndTruck",
"TruckLance Code is required": "TruckLance Code is required",
"Truck shop details updated successfully": "Truck shop details updated successfully",
"Failed to save truck shop details": "Failed to save truck shop details",
"Truck lane information is required": "Truck lane information is required",
"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",
"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",
"Edit shop details": "Edit shop details",
"Add Shop to Truck Lane": "Add Shop to Truck Lane"
}

+ 13
- 1
src/i18n/zh/common.json Visa fil

@@ -388,5 +388,17 @@
"Equipment Information": "設備資訊",
"Loading": "載入中...",
"Equipment not found": "找不到設備",
"Error saving data": "保存數據時出錯"
"Error saving data": "保存數據時出錯",
"TruckLance Code is required": "需要卡車路線編號",
"Truck shop details updated successfully": "卡車店鋪詳情更新成功",
"Failed to save truck shop details": "儲存卡車店鋪詳情失敗",
"Truck lane information is required": "需要卡車路線資訊",
"Shop added to truck lane successfully": "店鋪已成功新增至卡車路線",
"Failed to create shop in truck lane": "新增店鋪至卡車路線失敗",
"Add Shop": "新增店鋪",
"Search or select shop name": "搜尋或選擇店鋪名稱",
"Search or select shop code": "搜尋或選擇店鋪編號",
"Search or select remark": "搜尋或選擇備註",
"Edit shop details": "編輯店鋪詳情",
"Add Shop to Truck Lane": "新增店鋪至卡車路線"
}

Laddar…
Avbryt
Spara