|
- "use client";
-
- import React, { useCallback, useState } from "react";
- import { Box, Typography, Skeleton, Alert, TextField } from "@mui/material";
- import dynamic from "next/dynamic";
- import dayjs from "dayjs";
- import Assignment from "@mui/icons-material/Assignment";
- import {
- fetchJobOrderByStatus,
- fetchJobOrderCountByDate,
- fetchJobOrderCreatedCompletedByDate,
- fetchJobMaterialPendingPickedByDate,
- fetchJobProcessPendingCompletedByDate,
- fetchJobEquipmentWorkingWorkedByDate,
- } from "@/app/api/chart/client";
- import ChartCard from "../_components/ChartCard";
- import DateRangeSelect from "../_components/DateRangeSelect";
- import { toDateRange, DEFAULT_RANGE_DAYS } from "../_components/constants";
-
- const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false });
-
- const PAGE_TITLE = "工單";
-
- type Criteria = {
- joCountByDate: { rangeDays: number };
- joCreatedCompleted: { rangeDays: number };
- joDetail: { rangeDays: number };
- };
-
- const defaultCriteria: Criteria = {
- joCountByDate: { rangeDays: DEFAULT_RANGE_DAYS },
- joCreatedCompleted: { rangeDays: DEFAULT_RANGE_DAYS },
- joDetail: { rangeDays: DEFAULT_RANGE_DAYS },
- };
-
- export default function JobOrderChartPage() {
- const [joTargetDate, setJoTargetDate] = useState<string>(() => dayjs().format("YYYY-MM-DD"));
- const [criteria, setCriteria] = useState<Criteria>(defaultCriteria);
- const [error, setError] = useState<string | null>(null);
- const [chartData, setChartData] = useState<{
- joStatus: { status: string; count: number }[];
- joCountByDate: { date: string; orderCount: number }[];
- joCreatedCompleted: { date: string; createdCount: number; completedCount: number }[];
- joMaterial: { date: string; pendingCount: number; pickedCount: number }[];
- joProcess: { date: string; pendingCount: number; completedCount: number }[];
- joEquipment: { date: string; workingCount: number; workedCount: number }[];
- }>({
- joStatus: [],
- joCountByDate: [],
- joCreatedCompleted: [],
- joMaterial: [],
- joProcess: [],
- joEquipment: [],
- });
- const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({});
-
- const updateCriteria = useCallback(
- <K extends keyof Criteria>(key: K, updater: (prev: Criteria[K]) => Criteria[K]) => {
- setCriteria((prev) => ({ ...prev, [key]: updater(prev[key]) }));
- },
- []
- );
- const setChartLoading = useCallback((key: string, value: boolean) => {
- setLoadingCharts((prev) => (prev[key] === value ? prev : { ...prev, [key]: value }));
- }, []);
-
- React.useEffect(() => {
- setChartLoading("joStatus", true);
- fetchJobOrderByStatus(joTargetDate)
- .then((data) =>
- setChartData((prev) => ({
- ...prev,
- joStatus: data as { status: string; count: number }[],
- }))
- )
- .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
- .finally(() => setChartLoading("joStatus", false));
- }, [joTargetDate, setChartLoading]);
-
- React.useEffect(() => {
- const { startDate: s, endDate: e } = toDateRange(criteria.joCountByDate.rangeDays);
- setChartLoading("joCountByDate", true);
- fetchJobOrderCountByDate(s, e)
- .then((data) =>
- setChartData((prev) => ({
- ...prev,
- joCountByDate: data as { date: string; orderCount: number }[],
- }))
- )
- .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
- .finally(() => setChartLoading("joCountByDate", false));
- }, [criteria.joCountByDate, setChartLoading]);
-
- React.useEffect(() => {
- const { startDate: s, endDate: e } = toDateRange(criteria.joCreatedCompleted.rangeDays);
- setChartLoading("joCreatedCompleted", true);
- fetchJobOrderCreatedCompletedByDate(s, e)
- .then((data) =>
- setChartData((prev) => ({
- ...prev,
- joCreatedCompleted: data as {
- date: string;
- createdCount: number;
- completedCount: number;
- }[],
- }))
- )
- .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
- .finally(() => setChartLoading("joCreatedCompleted", false));
- }, [criteria.joCreatedCompleted, setChartLoading]);
-
- React.useEffect(() => {
- const { startDate: s, endDate: e } = toDateRange(criteria.joDetail.rangeDays);
- setChartLoading("joMaterial", true);
- fetchJobMaterialPendingPickedByDate(s, e)
- .then((data) =>
- setChartData((prev) => ({
- ...prev,
- joMaterial: data as { date: string; pendingCount: number; pickedCount: number }[],
- }))
- )
- .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
- .finally(() => setChartLoading("joMaterial", false));
- }, [criteria.joDetail, setChartLoading]);
-
- React.useEffect(() => {
- const { startDate: s, endDate: e } = toDateRange(criteria.joDetail.rangeDays);
- setChartLoading("joProcess", true);
- fetchJobProcessPendingCompletedByDate(s, e)
- .then((data) =>
- setChartData((prev) => ({
- ...prev,
- joProcess: data as { date: string; pendingCount: number; completedCount: number }[],
- }))
- )
- .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
- .finally(() => setChartLoading("joProcess", false));
- }, [criteria.joDetail, setChartLoading]);
-
- React.useEffect(() => {
- const { startDate: s, endDate: e } = toDateRange(criteria.joDetail.rangeDays);
- setChartLoading("joEquipment", true);
- fetchJobEquipmentWorkingWorkedByDate(s, e)
- .then((data) =>
- setChartData((prev) => ({
- ...prev,
- joEquipment: data as { date: string; workingCount: number; workedCount: number }[],
- }))
- )
- .catch((err) => setError(err instanceof Error ? err.message : "Request failed"))
- .finally(() => setChartLoading("joEquipment", false));
- }, [criteria.joDetail, setChartLoading]);
-
- return (
- <Box sx={{ maxWidth: 1200, mx: "auto" }}>
- <Typography variant="h5" sx={{ mb: 2, fontWeight: 600, display: "flex", alignItems: "center", gap: 1 }}>
- <Assignment /> {PAGE_TITLE}
- </Typography>
- {error && (
- <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
- {error}
- </Alert>
- )}
-
- <ChartCard
- title="工單按狀態"
- exportFilename="工單_按狀態"
- exportData={chartData.joStatus.map((p) => ({ 狀態: p.status, 數量: p.count }))}
- filters={
- <TextField
- size="small"
- label="日期(計劃開始)"
- type="date"
- value={joTargetDate}
- onChange={(e) => setJoTargetDate(e.target.value)}
- InputLabelProps={{ shrink: true }}
- sx={{ minWidth: 180 }}
- />
- }
- >
- {loadingCharts.joStatus ? (
- <Skeleton variant="rectangular" height={320} />
- ) : (
- <ApexCharts
- options={{
- chart: { type: "donut" },
- labels: chartData.joStatus.map((p) => p.status),
- legend: { position: "bottom" },
- }}
- series={chartData.joStatus.map((p) => p.count)}
- type="donut"
- width="100%"
- height={320}
- />
- )}
- </ChartCard>
-
- <ChartCard
- title="按日期工單數量(計劃開始日)"
- exportFilename="工單數量_按日期"
- exportData={chartData.joCountByDate.map((d) => ({ 日期: d.date, 工單數: d.orderCount }))}
- filters={
- <DateRangeSelect
- value={criteria.joCountByDate.rangeDays}
- onChange={(v) => updateCriteria("joCountByDate", (c) => ({ ...c, rangeDays: v }))}
- />
- }
- >
- {loadingCharts.joCountByDate ? (
- <Skeleton variant="rectangular" height={320} />
- ) : (
- <ApexCharts
- options={{
- chart: { type: "bar" },
- xaxis: { categories: chartData.joCountByDate.map((d) => d.date) },
- yaxis: { title: { text: "單數" } },
- plotOptions: { bar: { columnWidth: "60%" } },
- dataLabels: { enabled: false },
- }}
- series={[{ name: "工單數", data: chartData.joCountByDate.map((d) => d.orderCount) }]}
- type="bar"
- width="100%"
- height={320}
- />
- )}
- </ChartCard>
-
- <ChartCard
- title="工單創建與完成按日期"
- exportFilename="工單創建與完成_按日期"
- exportData={chartData.joCreatedCompleted.map((d) => ({ 日期: d.date, 創建: d.createdCount, 完成: d.completedCount }))}
- filters={
- <DateRangeSelect
- value={criteria.joCreatedCompleted.rangeDays}
- onChange={(v) => updateCriteria("joCreatedCompleted", (c) => ({ ...c, rangeDays: v }))}
- />
- }
- >
- {loadingCharts.joCreatedCompleted ? (
- <Skeleton variant="rectangular" height={320} />
- ) : (
- <ApexCharts
- options={{
- chart: { type: "line" },
- xaxis: { categories: chartData.joCreatedCompleted.map((d) => d.date) },
- yaxis: { title: { text: "數量" } },
- stroke: { curve: "smooth" },
- dataLabels: { enabled: false },
- }}
- series={[
- { name: "創建", data: chartData.joCreatedCompleted.map((d) => d.createdCount) },
- { name: "完成", data: chartData.joCreatedCompleted.map((d) => d.completedCount) },
- ]}
- type="line"
- width="100%"
- height={320}
- />
- )}
- </ChartCard>
-
- <Typography variant="h6" sx={{ mt: 3, mb: 1, fontWeight: 600 }}>
- 工單物料/工序/設備
- </Typography>
- <ChartCard
- title="物料待領/已揀(按工單計劃日)"
- exportFilename="工單物料_待領已揀_按日期"
- exportData={chartData.joMaterial.map((d) => ({ 日期: d.date, 待領: d.pendingCount, 已揀: d.pickedCount }))}
- filters={
- <DateRangeSelect
- value={criteria.joDetail.rangeDays}
- onChange={(v) => updateCriteria("joDetail", (c) => ({ ...c, rangeDays: v }))}
- />
- }
- >
- {loadingCharts.joMaterial ? (
- <Skeleton variant="rectangular" height={320} />
- ) : (
- <ApexCharts
- options={{
- chart: { type: "bar" },
- xaxis: { categories: chartData.joMaterial.map((d) => d.date) },
- yaxis: { title: { text: "筆數" } },
- plotOptions: { bar: { columnWidth: "60%" } },
- dataLabels: { enabled: false },
- legend: { position: "top" },
- }}
- series={[
- { name: "待領", data: chartData.joMaterial.map((d) => d.pendingCount) },
- { name: "已揀", data: chartData.joMaterial.map((d) => d.pickedCount) },
- ]}
- type="bar"
- width="100%"
- height={320}
- />
- )}
- </ChartCard>
-
- <ChartCard
- title="工序待完成/已完成(按工單計劃日)"
- exportFilename="工單工序_待完成已完成_按日期"
- exportData={chartData.joProcess.map((d) => ({ 日期: d.date, 待完成: d.pendingCount, 已完成: d.completedCount }))}
- filters={
- <DateRangeSelect
- value={criteria.joDetail.rangeDays}
- onChange={(v) => updateCriteria("joDetail", (c) => ({ ...c, rangeDays: v }))}
- />
- }
- >
- {loadingCharts.joProcess ? (
- <Skeleton variant="rectangular" height={320} />
- ) : (
- <ApexCharts
- options={{
- chart: { type: "bar" },
- xaxis: { categories: chartData.joProcess.map((d) => d.date) },
- yaxis: { title: { text: "筆數" } },
- plotOptions: { bar: { columnWidth: "60%" } },
- dataLabels: { enabled: false },
- legend: { position: "top" },
- }}
- series={[
- { name: "待完成", data: chartData.joProcess.map((d) => d.pendingCount) },
- { name: "已完成", data: chartData.joProcess.map((d) => d.completedCount) },
- ]}
- type="bar"
- width="100%"
- height={320}
- />
- )}
- </ChartCard>
-
- <ChartCard
- title="設備使用中/已使用(按工單)"
- exportFilename="工單設備_使用中已使用_按日期"
- exportData={chartData.joEquipment.map((d) => ({ 日期: d.date, 使用中: d.workingCount, 已使用: d.workedCount }))}
- filters={
- <DateRangeSelect
- value={criteria.joDetail.rangeDays}
- onChange={(v) => updateCriteria("joDetail", (c) => ({ ...c, rangeDays: v }))}
- />
- }
- >
- {loadingCharts.joEquipment ? (
- <Skeleton variant="rectangular" height={320} />
- ) : (
- <ApexCharts
- options={{
- chart: { type: "bar" },
- xaxis: { categories: chartData.joEquipment.map((d) => d.date) },
- yaxis: { title: { text: "筆數" } },
- plotOptions: { bar: { columnWidth: "60%" } },
- dataLabels: { enabled: false },
- legend: { position: "top" },
- }}
- series={[
- { name: "使用中", data: chartData.joEquipment.map((d) => d.workingCount) },
- { name: "已使用", data: chartData.joEquipment.map((d) => d.workedCount) },
- ]}
- type="bar"
- width="100%"
- height={320}
- />
- )}
- </ChartCard>
- </Box>
- );
- }
|