FPSMS-frontend
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

387 lines
14 KiB

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