FPSMS-frontend
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 

368 рядки
14 KiB

  1. "use client";
  2. import React, { useCallback, useState } from "react";
  3. import { Box, Typography, Skeleton, Alert, TextField } from "@mui/material";
  4. import dynamic from "next/dynamic";
  5. import dayjs from "dayjs";
  6. import Assignment from "@mui/icons-material/Assignment";
  7. import {
  8. fetchJobOrderByStatus,
  9. fetchJobOrderCountByDate,
  10. fetchJobOrderCreatedCompletedByDate,
  11. fetchJobMaterialPendingPickedByDate,
  12. fetchJobProcessPendingCompletedByDate,
  13. fetchJobEquipmentWorkingWorkedByDate,
  14. } from "@/app/api/chart/client";
  15. import ChartCard from "../_components/ChartCard";
  16. import DateRangeSelect from "../_components/DateRangeSelect";
  17. import { toDateRange, DEFAULT_RANGE_DAYS } from "../_components/constants";
  18. const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false });
  19. const PAGE_TITLE = "工單";
  20. type Criteria = {
  21. joCountByDate: { rangeDays: number };
  22. joCreatedCompleted: { rangeDays: number };
  23. joDetail: { rangeDays: number };
  24. };
  25. const defaultCriteria: Criteria = {
  26. joCountByDate: { rangeDays: DEFAULT_RANGE_DAYS },
  27. joCreatedCompleted: { rangeDays: DEFAULT_RANGE_DAYS },
  28. joDetail: { rangeDays: DEFAULT_RANGE_DAYS },
  29. };
  30. export default function JobOrderChartPage() {
  31. const [joTargetDate, setJoTargetDate] = useState<string>(() => dayjs().format("YYYY-MM-DD"));
  32. const [criteria, setCriteria] = useState<Criteria>(defaultCriteria);
  33. const [error, setError] = useState<string | null>(null);
  34. const [chartData, setChartData] = useState<{
  35. joStatus: { status: string; count: number }[];
  36. joCountByDate: { date: string; orderCount: number }[];
  37. joCreatedCompleted: { date: string; createdCount: number; completedCount: number }[];
  38. joMaterial: { date: string; pendingCount: number; pickedCount: number }[];
  39. joProcess: { date: string; pendingCount: number; completedCount: number }[];
  40. joEquipment: { date: string; workingCount: number; workedCount: number }[];
  41. }>({
  42. joStatus: [],
  43. joCountByDate: [],
  44. joCreatedCompleted: [],
  45. joMaterial: [],
  46. joProcess: [],
  47. joEquipment: [],
  48. });
  49. const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({});
  50. const updateCriteria = useCallback(
  51. <K extends keyof Criteria>(key: K, updater: (prev: Criteria[K]) => Criteria[K]) => {
  52. setCriteria((prev) => ({ ...prev, [key]: updater(prev[key]) }));
  53. },
  54. []
  55. );
  56. const setChartLoading = useCallback((key: string, value: boolean) => {
  57. setLoadingCharts((prev) => (prev[key] === value ? prev : { ...prev, [key]: value }));
  58. }, []);
  59. React.useEffect(() => {
  60. setChartLoading("joStatus", true);
  61. fetchJobOrderByStatus(joTargetDate)
  62. .then((data) =>
  63. setChartData((prev) => ({
  64. ...prev,
  65. joStatus: data as { status: string; count: number }[],
  66. }))
  67. )
  68. .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
  69. .finally(() => setChartLoading("joStatus", false));
  70. }, [joTargetDate, setChartLoading]);
  71. React.useEffect(() => {
  72. const { startDate: s, endDate: e } = toDateRange(criteria.joCountByDate.rangeDays);
  73. setChartLoading("joCountByDate", true);
  74. fetchJobOrderCountByDate(s, e)
  75. .then((data) =>
  76. setChartData((prev) => ({
  77. ...prev,
  78. joCountByDate: data as { date: string; orderCount: number }[],
  79. }))
  80. )
  81. .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
  82. .finally(() => setChartLoading("joCountByDate", false));
  83. }, [criteria.joCountByDate, setChartLoading]);
  84. React.useEffect(() => {
  85. const { startDate: s, endDate: e } = toDateRange(criteria.joCreatedCompleted.rangeDays);
  86. setChartLoading("joCreatedCompleted", true);
  87. fetchJobOrderCreatedCompletedByDate(s, e)
  88. .then((data) =>
  89. setChartData((prev) => ({
  90. ...prev,
  91. joCreatedCompleted: data as {
  92. date: string;
  93. createdCount: number;
  94. completedCount: number;
  95. }[],
  96. }))
  97. )
  98. .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
  99. .finally(() => setChartLoading("joCreatedCompleted", false));
  100. }, [criteria.joCreatedCompleted, setChartLoading]);
  101. React.useEffect(() => {
  102. const { startDate: s, endDate: e } = toDateRange(criteria.joDetail.rangeDays);
  103. setChartLoading("joMaterial", true);
  104. fetchJobMaterialPendingPickedByDate(s, e)
  105. .then((data) =>
  106. setChartData((prev) => ({
  107. ...prev,
  108. joMaterial: data as { date: string; pendingCount: number; pickedCount: number }[],
  109. }))
  110. )
  111. .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
  112. .finally(() => setChartLoading("joMaterial", false));
  113. }, [criteria.joDetail, setChartLoading]);
  114. React.useEffect(() => {
  115. const { startDate: s, endDate: e } = toDateRange(criteria.joDetail.rangeDays);
  116. setChartLoading("joProcess", true);
  117. fetchJobProcessPendingCompletedByDate(s, e)
  118. .then((data) =>
  119. setChartData((prev) => ({
  120. ...prev,
  121. joProcess: data as { date: string; pendingCount: number; completedCount: number }[],
  122. }))
  123. )
  124. .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
  125. .finally(() => setChartLoading("joProcess", false));
  126. }, [criteria.joDetail, setChartLoading]);
  127. React.useEffect(() => {
  128. const { startDate: s, endDate: e } = toDateRange(criteria.joDetail.rangeDays);
  129. setChartLoading("joEquipment", true);
  130. fetchJobEquipmentWorkingWorkedByDate(s, e)
  131. .then((data) =>
  132. setChartData((prev) => ({
  133. ...prev,
  134. joEquipment: data as { date: string; workingCount: number; workedCount: number }[],
  135. }))
  136. )
  137. .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
  138. .finally(() => setChartLoading("joEquipment", false));
  139. }, [criteria.joDetail, setChartLoading]);
  140. return (
  141. <Box sx={{ maxWidth: 1200, mx: "auto" }}>
  142. <Typography variant="h5" sx={{ mb: 2, fontWeight: 600, display: "flex", alignItems: "center", gap: 1 }}>
  143. <Assignment /> {PAGE_TITLE}
  144. </Typography>
  145. {error && (
  146. <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
  147. {error}
  148. </Alert>
  149. )}
  150. <ChartCard
  151. title="工單按狀態"
  152. exportFilename="工單_按狀態"
  153. exportData={chartData.joStatus.map((p) => ({ 狀態: p.status, 數量: p.count }))}
  154. filters={
  155. <TextField
  156. size="small"
  157. label="日期(計劃開始)"
  158. type="date"
  159. value={joTargetDate}
  160. onChange={(e) => setJoTargetDate(e.target.value)}
  161. InputLabelProps={{ shrink: true }}
  162. sx={{ minWidth: 180 }}
  163. />
  164. }
  165. >
  166. {loadingCharts.joStatus ? (
  167. <Skeleton variant="rectangular" height={320} />
  168. ) : (
  169. <ApexCharts
  170. options={{
  171. chart: { type: "donut" },
  172. labels: chartData.joStatus.map((p) => p.status),
  173. legend: { position: "bottom" },
  174. }}
  175. series={chartData.joStatus.map((p) => p.count)}
  176. type="donut"
  177. width="100%"
  178. height={320}
  179. />
  180. )}
  181. </ChartCard>
  182. <ChartCard
  183. title="按日期工單數量(計劃開始日)"
  184. exportFilename="工單數量_按日期"
  185. exportData={chartData.joCountByDate.map((d) => ({ 日期: d.date, 工單數: d.orderCount }))}
  186. filters={
  187. <DateRangeSelect
  188. value={criteria.joCountByDate.rangeDays}
  189. onChange={(v) => updateCriteria("joCountByDate", (c) => ({ ...c, rangeDays: v }))}
  190. />
  191. }
  192. >
  193. {loadingCharts.joCountByDate ? (
  194. <Skeleton variant="rectangular" height={320} />
  195. ) : (
  196. <ApexCharts
  197. options={{
  198. chart: { type: "bar" },
  199. xaxis: { categories: chartData.joCountByDate.map((d) => d.date) },
  200. yaxis: { title: { text: "單數" } },
  201. plotOptions: { bar: { columnWidth: "60%" } },
  202. dataLabels: { enabled: false },
  203. }}
  204. series={[{ name: "工單數", data: chartData.joCountByDate.map((d) => d.orderCount) }]}
  205. type="bar"
  206. width="100%"
  207. height={320}
  208. />
  209. )}
  210. </ChartCard>
  211. <ChartCard
  212. title="工單創建與完成按日期"
  213. exportFilename="工單創建與完成_按日期"
  214. exportData={chartData.joCreatedCompleted.map((d) => ({ 日期: d.date, 創建: d.createdCount, 完成: d.completedCount }))}
  215. filters={
  216. <DateRangeSelect
  217. value={criteria.joCreatedCompleted.rangeDays}
  218. onChange={(v) => updateCriteria("joCreatedCompleted", (c) => ({ ...c, rangeDays: v }))}
  219. />
  220. }
  221. >
  222. {loadingCharts.joCreatedCompleted ? (
  223. <Skeleton variant="rectangular" height={320} />
  224. ) : (
  225. <ApexCharts
  226. options={{
  227. chart: { type: "line" },
  228. xaxis: { categories: chartData.joCreatedCompleted.map((d) => d.date) },
  229. yaxis: { title: { text: "數量" } },
  230. stroke: { curve: "smooth" },
  231. dataLabels: { enabled: false },
  232. }}
  233. series={[
  234. { name: "創建", data: chartData.joCreatedCompleted.map((d) => d.createdCount) },
  235. { name: "完成", data: chartData.joCreatedCompleted.map((d) => d.completedCount) },
  236. ]}
  237. type="line"
  238. width="100%"
  239. height={320}
  240. />
  241. )}
  242. </ChartCard>
  243. <Typography variant="h6" sx={{ mt: 3, mb: 1, fontWeight: 600 }}>
  244. 工單物料/工序/設備
  245. </Typography>
  246. <ChartCard
  247. title="物料待領/已揀(按工單計劃日)"
  248. exportFilename="工單物料_待領已揀_按日期"
  249. exportData={chartData.joMaterial.map((d) => ({ 日期: d.date, 待領: d.pendingCount, 已揀: d.pickedCount }))}
  250. filters={
  251. <DateRangeSelect
  252. value={criteria.joDetail.rangeDays}
  253. onChange={(v) => updateCriteria("joDetail", (c) => ({ ...c, rangeDays: v }))}
  254. />
  255. }
  256. >
  257. {loadingCharts.joMaterial ? (
  258. <Skeleton variant="rectangular" height={320} />
  259. ) : (
  260. <ApexCharts
  261. options={{
  262. chart: { type: "bar" },
  263. xaxis: { categories: chartData.joMaterial.map((d) => d.date) },
  264. yaxis: { title: { text: "筆數" } },
  265. plotOptions: { bar: { columnWidth: "60%" } },
  266. dataLabels: { enabled: false },
  267. legend: { position: "top" },
  268. }}
  269. series={[
  270. { name: "待領", data: chartData.joMaterial.map((d) => d.pendingCount) },
  271. { name: "已揀", data: chartData.joMaterial.map((d) => d.pickedCount) },
  272. ]}
  273. type="bar"
  274. width="100%"
  275. height={320}
  276. />
  277. )}
  278. </ChartCard>
  279. <ChartCard
  280. title="工序待完成/已完成(按工單計劃日)"
  281. exportFilename="工單工序_待完成已完成_按日期"
  282. exportData={chartData.joProcess.map((d) => ({ 日期: d.date, 待完成: d.pendingCount, 已完成: d.completedCount }))}
  283. filters={
  284. <DateRangeSelect
  285. value={criteria.joDetail.rangeDays}
  286. onChange={(v) => updateCriteria("joDetail", (c) => ({ ...c, rangeDays: v }))}
  287. />
  288. }
  289. >
  290. {loadingCharts.joProcess ? (
  291. <Skeleton variant="rectangular" height={320} />
  292. ) : (
  293. <ApexCharts
  294. options={{
  295. chart: { type: "bar" },
  296. xaxis: { categories: chartData.joProcess.map((d) => d.date) },
  297. yaxis: { title: { text: "筆數" } },
  298. plotOptions: { bar: { columnWidth: "60%" } },
  299. dataLabels: { enabled: false },
  300. legend: { position: "top" },
  301. }}
  302. series={[
  303. { name: "待完成", data: chartData.joProcess.map((d) => d.pendingCount) },
  304. { name: "已完成", data: chartData.joProcess.map((d) => d.completedCount) },
  305. ]}
  306. type="bar"
  307. width="100%"
  308. height={320}
  309. />
  310. )}
  311. </ChartCard>
  312. <ChartCard
  313. title="設備使用中/已使用(按工單)"
  314. exportFilename="工單設備_使用中已使用_按日期"
  315. exportData={chartData.joEquipment.map((d) => ({ 日期: d.date, 使用中: d.workingCount, 已使用: d.workedCount }))}
  316. filters={
  317. <DateRangeSelect
  318. value={criteria.joDetail.rangeDays}
  319. onChange={(v) => updateCriteria("joDetail", (c) => ({ ...c, rangeDays: v }))}
  320. />
  321. }
  322. >
  323. {loadingCharts.joEquipment ? (
  324. <Skeleton variant="rectangular" height={320} />
  325. ) : (
  326. <ApexCharts
  327. options={{
  328. chart: { type: "bar" },
  329. xaxis: { categories: chartData.joEquipment.map((d) => d.date) },
  330. yaxis: { title: { text: "筆數" } },
  331. plotOptions: { bar: { columnWidth: "60%" } },
  332. dataLabels: { enabled: false },
  333. legend: { position: "top" },
  334. }}
  335. series={[
  336. { name: "使用中", data: chartData.joEquipment.map((d) => d.workingCount) },
  337. { name: "已使用", data: chartData.joEquipment.map((d) => d.workedCount) },
  338. ]}
  339. type="bar"
  340. width="100%"
  341. height={320}
  342. />
  343. )}
  344. </ChartCard>
  345. </Box>
  346. );
  347. }