FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

405 rivejä
13 KiB

  1. "use client";
  2. import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
  3. import {
  4. Box,
  5. Button,
  6. Card,
  7. CardContent,
  8. Stack,
  9. Typography,
  10. Alert,
  11. CircularProgress,
  12. Chip,
  13. Tabs,
  14. Tab,
  15. Select,
  16. MenuItem,
  17. FormControl,
  18. InputLabel,
  19. } from "@mui/material";
  20. import { useState, useMemo, useCallback, useEffect } from "react";
  21. import { useRouter, useSearchParams } from "next/navigation";
  22. import { useTranslation } from "react-i18next";
  23. import SearchBox, { Criterion } from "../SearchBox";
  24. import SearchResults, { Column } from "../SearchResults";
  25. import { defaultPagingController } from "../SearchResults/SearchResults";
  26. import { fetchAllShopsClient } from "@/app/api/shop/client";
  27. import type { Shop, ShopAndTruck } from "@/app/api/shop/actions";
  28. import TruckLane from "./TruckLane";
  29. type ShopRow = Shop & {
  30. actions?: string;
  31. truckLanceStatus?: "complete" | "missing" | "no-truck";
  32. };
  33. type SearchQuery = {
  34. id: string;
  35. name: string;
  36. code: string;
  37. };
  38. type SearchParamNames = keyof SearchQuery;
  39. const Shop: React.FC = () => {
  40. const { t } = useTranslation("shop");
  41. const router = useRouter();
  42. const searchParams = useSearchParams();
  43. const [activeTab, setActiveTab] = useState<number>(0);
  44. const [rows, setRows] = useState<ShopRow[]>([]);
  45. const [loading, setLoading] = useState<boolean>(false);
  46. const [error, setError] = useState<string | null>(null);
  47. const [filters, setFilters] = useState<Record<string, string>>({});
  48. const [statusFilter, setStatusFilter] = useState<string>("all");
  49. const [pagingController, setPagingController] = useState(defaultPagingController);
  50. // client-side filtered rows (contains-matching + status filter)
  51. const filteredRows = useMemo(() => {
  52. const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== "");
  53. let normalized = (rows || []).filter((r) => {
  54. // apply contains matching for each active filter
  55. for (const k of fKeys) {
  56. const v = String((filters as any)[k] ?? "").trim();
  57. const rv = String((r as any)[k] ?? "").trim();
  58. // Use exact matching for id field, contains matching for others
  59. if (k === "id") {
  60. const numValue = Number(v);
  61. const rvNum = Number(rv);
  62. if (!isNaN(numValue) && !isNaN(rvNum)) {
  63. if (numValue !== rvNum) return false;
  64. } else {
  65. if (v !== rv) return false;
  66. }
  67. } else {
  68. if (!rv.toLowerCase().includes(v.toLowerCase())) return false;
  69. }
  70. }
  71. return true;
  72. });
  73. // Apply status filter
  74. if (statusFilter !== "all") {
  75. normalized = normalized.filter((r) => {
  76. return r.truckLanceStatus === statusFilter;
  77. });
  78. }
  79. return normalized;
  80. }, [rows, filters, statusFilter]);
  81. // Check if a shop has missing truckLanceCode data
  82. const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => {
  83. if (!shopTrucks || shopTrucks.length === 0) {
  84. return "no-truck";
  85. }
  86. // Check if shop has any actual truck lanes (not just null entries from LEFT JOIN)
  87. // A shop with no trucks will have entries with null truckLanceCode
  88. const hasAnyTruckLane = shopTrucks.some((truck) => {
  89. const truckLanceCode = (truck as any).truckLanceCode;
  90. return truckLanceCode != null && String(truckLanceCode).trim() !== "";
  91. });
  92. if (!hasAnyTruckLane) {
  93. return "no-truck";
  94. }
  95. // Check each truckLanceCode entry for missing data
  96. for (const truck of shopTrucks) {
  97. // Skip entries without truckLanceCode (they're from LEFT JOIN when no trucks exist)
  98. const truckLanceCode = (truck as any).truckLanceCode;
  99. if (!truckLanceCode || String(truckLanceCode).trim() === "") {
  100. continue; // Skip this entry, it's not a real truck lane
  101. }
  102. // Check truckLanceCode: must exist and not be empty (already validated above)
  103. const hasTruckLanceCode = truckLanceCode != null && String(truckLanceCode).trim() !== "";
  104. // Check departureTime: must exist and not be empty
  105. // Can be array format [hours, minutes] or string format
  106. const departureTime = (truck as any).departureTime || (truck as any).DepartureTime;
  107. let hasDepartureTime = false;
  108. if (departureTime != null) {
  109. if (Array.isArray(departureTime) && departureTime.length >= 2) {
  110. // Array format [hours, minutes]
  111. hasDepartureTime = true;
  112. } else {
  113. // String format
  114. const timeStr = String(departureTime).trim();
  115. hasDepartureTime = timeStr !== "" && timeStr !== "-";
  116. }
  117. }
  118. // Check loadingSequence: must exist and not be 0
  119. const loadingSeq = (truck as any).loadingSequence || (truck as any).LoadingSequence;
  120. const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null;
  121. const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0;
  122. // Check storeId: must exist and not be 0 (can be string FLOOR_A_KEY/FLOOR_B_KEY or number)
  123. // Actual field name in JSON is store_id (underscore, lowercase)
  124. const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id;
  125. let storeIdValid = false;
  126. if (storeId != null && storeId !== undefined && storeId !== "") {
  127. const storeIdStr = String(storeId).trim();
  128. // If it's FLOOR_A_KEY or FLOOR_B_KEY, it's valid (not 0)
  129. if (storeIdStr === FLOOR_A_KEY || storeIdStr === FLOOR_B_KEY) {
  130. storeIdValid = true;
  131. } else {
  132. const storeIdNum = Number(storeId);
  133. // If it's a valid number and not 0, it's valid
  134. if (!isNaN(storeIdNum) && storeIdNum !== 0) {
  135. storeIdValid = true;
  136. }
  137. }
  138. }
  139. // If any required field is missing or equals 0, return "missing"
  140. if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !storeIdValid) {
  141. return "missing";
  142. }
  143. }
  144. return "complete";
  145. }, []);
  146. const fetchAllShops = async (params?: Record<string, string>) => {
  147. setLoading(true);
  148. setError(null);
  149. try {
  150. const data = await fetchAllShopsClient(params) as ShopAndTruck[];
  151. console.log("Fetched shops data:", data);
  152. // Group data by shop ID (one shop can have multiple TruckLanceCode entries)
  153. const shopMap = new Map<number, { shop: Shop; trucks: ShopAndTruck[] }>();
  154. (data || []).forEach((item: ShopAndTruck) => {
  155. const shopId = item.id;
  156. if (!shopMap.has(shopId)) {
  157. shopMap.set(shopId, {
  158. shop: {
  159. id: item.id,
  160. name: item.name,
  161. code: item.code,
  162. addr3: item.addr3 ?? "",
  163. },
  164. trucks: [],
  165. });
  166. }
  167. shopMap.get(shopId)!.trucks.push(item);
  168. });
  169. // Convert to ShopRow array with truckLanceStatus
  170. const mapped: ShopRow[] = Array.from(shopMap.values()).map(({ shop, trucks }) => ({
  171. ...shop,
  172. truckLanceStatus: checkTruckLanceStatus(trucks),
  173. }));
  174. setRows(mapped);
  175. } catch (err: any) {
  176. console.error("Failed to load shops:", err);
  177. setError(err?.message ?? String(err));
  178. } finally {
  179. setLoading(false);
  180. }
  181. };
  182. // SearchBox onSearch will call this
  183. const handleSearch = (inputs: Record<string, string>) => {
  184. setFilters(inputs);
  185. setPagingController({ ...pagingController, pageNum: 1 });
  186. const params: Record<string, string> = {};
  187. Object.entries(inputs || {}).forEach(([k, v]) => {
  188. if (v != null && String(v).trim() !== "") params[k] = String(v).trim();
  189. });
  190. if (Object.keys(params).length === 0) fetchAllShops();
  191. else fetchAllShops(params);
  192. };
  193. const handleViewDetail = useCallback(
  194. (shop: ShopRow) => {
  195. router.push(`/settings/shop/detail?id=${shop.id}`);
  196. },
  197. [router]
  198. );
  199. const criteria: Criterion<SearchParamNames>[] = [
  200. { type: "text", label: t("id"), paramName: "id" },
  201. { type: "text", label: t("code"), paramName: "code" },
  202. { type: "text", label: t("Shop Name"), paramName: "name" },
  203. ];
  204. const columns: Column<ShopRow>[] = [
  205. {
  206. name: "id",
  207. label: t("id"),
  208. type: "integer",
  209. sx: { width: "100px", minWidth: "100px", maxWidth: "100px" },
  210. renderCell: (item) => String(item.id ?? ""),
  211. },
  212. {
  213. name: "code",
  214. label: t("Code"),
  215. sx: { width: "150px", minWidth: "150px", maxWidth: "150px" },
  216. renderCell: (item) => String(item.code ?? ""),
  217. },
  218. {
  219. name: "name",
  220. label: t("Name"),
  221. sx: { width: "200px", minWidth: "200px", maxWidth: "200px" },
  222. renderCell: (item) => String(item.name ?? ""),
  223. },
  224. {
  225. name: "addr3",
  226. label: t("Addr3"),
  227. sx: { width: "200px", minWidth: "200px", maxWidth: "200px" },
  228. renderCell: (item) => String((item as any).addr3 ?? ""),
  229. },
  230. {
  231. name: "truckLanceStatus",
  232. label: t("TruckLance Status"),
  233. align: "center",
  234. headerAlign: "center",
  235. sx: { width: "150px", minWidth: "150px", maxWidth: "150px" },
  236. renderCell: (item) => {
  237. const status = item.truckLanceStatus;
  238. if (status === "complete") {
  239. return <Chip label={t("Complete")} color="success" size="small" />;
  240. } else if (status === "missing") {
  241. return <Chip label={t("Missing Data")} color="warning" size="small" />;
  242. } else {
  243. return <Chip label={t("No TruckLance")} color="error" size="small" />;
  244. }
  245. },
  246. },
  247. {
  248. name: "actions",
  249. label: t("Actions"),
  250. align: "right",
  251. headerAlign: "right",
  252. sx: { width: "150px", minWidth: "150px", maxWidth: "150px" },
  253. renderCell: (item) => (
  254. <Button
  255. size="small"
  256. variant="outlined"
  257. onClick={() => handleViewDetail(item)}
  258. >
  259. {t("View Detail")}
  260. </Button>
  261. ),
  262. },
  263. ];
  264. // Initialize activeTab from URL parameter
  265. useEffect(() => {
  266. const tabParam = searchParams.get("tab");
  267. if (tabParam !== null) {
  268. const tabIndex = parseInt(tabParam, 10);
  269. if (!isNaN(tabIndex) && (tabIndex === 0 || tabIndex === 1)) {
  270. setActiveTab(tabIndex);
  271. }
  272. }
  273. }, [searchParams]);
  274. const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
  275. setActiveTab(newValue);
  276. // Update URL to reflect the selected tab
  277. const url = new URL(window.location.href);
  278. url.searchParams.set("tab", String(newValue));
  279. router.push(url.pathname + url.search);
  280. };
  281. return (
  282. <Box>
  283. {/* Header section with title */}
  284. <Box sx={{
  285. p: 2,
  286. borderBottom: '1px solid #e0e0e0'
  287. }}>
  288. <Stack direction="row" alignItems="center" justifyContent="space-between" spacing={2}>
  289. <Typography variant="h4">{t("ShopAndTruck")}</Typography>
  290. <Button variant="outlined" onClick={() => router.push("/settings/shop/board")}>
  291. {t("Route Board")}
  292. </Button>
  293. </Stack>
  294. </Box>
  295. {/* Tabs section */}
  296. <Box sx={{
  297. borderBottom: '1px solid #e0e0e0'
  298. }}>
  299. <Tabs
  300. value={activeTab}
  301. onChange={handleTabChange}
  302. >
  303. <Tab label={t("Shop")} />
  304. <Tab label={t("Truck Lane")} />
  305. </Tabs>
  306. </Box>
  307. {/* Content section */}
  308. <Box sx={{ p: 2 }}>
  309. {activeTab === 0 && (
  310. <>
  311. <Card sx={{ mb: 2 }}>
  312. <CardContent>
  313. <SearchBox
  314. criteria={criteria as Criterion<string>[]}
  315. onSearch={handleSearch}
  316. onReset={() => {
  317. setRows([]);
  318. setFilters({});
  319. }}
  320. />
  321. </CardContent>
  322. </Card>
  323. <Card>
  324. <CardContent>
  325. <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
  326. <Typography variant="h6">{t("Shop")}</Typography>
  327. <FormControl size="small" sx={{ minWidth: 200 }}>
  328. <InputLabel>{t("Filter by Status")}</InputLabel>
  329. <Select
  330. value={statusFilter}
  331. label={t("Filter by Status")}
  332. onChange={(e) => setStatusFilter(e.target.value)}
  333. >
  334. <MenuItem value="all">{t("All")}</MenuItem>
  335. <MenuItem value="complete">{t("Complete")}</MenuItem>
  336. <MenuItem value="missing">{t("Missing Data")}</MenuItem>
  337. <MenuItem value="no-truck">{t("No TruckLance")}</MenuItem>
  338. </Select>
  339. </FormControl>
  340. </Stack>
  341. {error && (
  342. <Alert severity="error" sx={{ mb: 2 }}>
  343. {error}
  344. </Alert>
  345. )}
  346. {loading ? (
  347. <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
  348. <CircularProgress />
  349. </Box>
  350. ) : (
  351. <SearchResults
  352. items={filteredRows}
  353. columns={columns}
  354. pagingController={pagingController}
  355. setPagingController={setPagingController}
  356. />
  357. )}
  358. </CardContent>
  359. </Card>
  360. </>
  361. )}
  362. {activeTab === 1 && (
  363. <TruckLane />
  364. )}
  365. </Box>
  366. </Box>
  367. );
  368. };
  369. export default Shop;