FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

373 строки
12 KiB

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