| @@ -3,6 +3,7 @@ import { getServerI18n } from "@/i18n"; | |||
| import { Stack } from "@mui/material"; | |||
| import { Metadata } from "next"; | |||
| import React, { Suspense } from "react"; | |||
| import { I18nProvider } from "@/i18n"; | |||
| export const metadata: Metadata = { | |||
| title: "Import Testing" | |||
| @@ -21,7 +22,9 @@ const M18ImportTestingPage: React.FC = async () => { | |||
| > | |||
| </Stack> | |||
| <Suspense fallback={<M18ImportTesting.Loading />}> | |||
| <M18ImportTesting /> | |||
| <I18nProvider namespaces={["common", "m18ImportTesting"]}> | |||
| <M18ImportTesting /> | |||
| </I18nProvider> | |||
| </Suspense> | |||
| </> | |||
| ) | |||
| @@ -0,0 +1,159 @@ | |||
| "use server"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| import { PoResult, StockInLine } from "."; | |||
| //import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { QcItemResult } from "../settings/qcItem"; | |||
| import { RecordsRes } from "../utils"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| export interface PostStockInLiineResponse<T> { | |||
| id: number | null; | |||
| name: string; | |||
| code: string; | |||
| type?: string | |||
| message: string | null; | |||
| errorPosition: string | keyof T; | |||
| entity: T | T[] | |||
| // entity: StockInLine | StockInLine[] | |||
| } | |||
| export interface StockInLineEntry { | |||
| id?: number | |||
| itemId: number | |||
| purchaseOrderId: number | |||
| purchaseOrderLineId: number | |||
| acceptedQty: number | |||
| status?: string | |||
| expiryDate?: string | |||
| } | |||
| export interface PurchaseQcResult { | |||
| qcItemId: number; | |||
| failQty: number; | |||
| } | |||
| export interface StockInInput { | |||
| status: string | |||
| productLotNo?: string, | |||
| receiptDate: string | |||
| acceptedQty: number | |||
| acceptedWeight?: number | |||
| productionDate?: string | |||
| expiryDate: string | |||
| } | |||
| export interface PurchaseQCInput { | |||
| status: string | |||
| acceptedQty: number | |||
| sampleRate: number; | |||
| sampleWeight: number; | |||
| totalWeight: number; | |||
| qcResult: PurchaseQcResult[]; | |||
| } | |||
| export interface EscalationInput { | |||
| status: string | |||
| handler: string | |||
| acceptedQty: number // this is the qty to be escalated | |||
| // escalationQty: number | |||
| } | |||
| export interface PutawayInput { | |||
| status: string | |||
| acceptedQty: number | |||
| warehouseId: number | |||
| // handler: string | |||
| // stockInLine: StockInLineEntry[] | |||
| } | |||
| export type ModalFormInput = Partial<PurchaseQCInput & StockInInput & EscalationInput & PutawayInput> | |||
| export const testFetch = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { | |||
| return serverFetchJson<StockInLine>(`${BASE_API_URL}/stockInLine/${stockInLineId}`, { | |||
| next: { tags: ["stockInLine"] }, | |||
| }); | |||
| }); | |||
| export const createStockInLine = async (data: StockInLineEntry) => { | |||
| const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry>>(`${BASE_API_URL}/stockInLine/create`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return stockInLine | |||
| } | |||
| export const updateStockInLine = async (data: StockInLineEntry & ModalFormInput) => { | |||
| const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry & ModalFormInput>>(`${BASE_API_URL}/stockInLine/update`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return stockInLine | |||
| } | |||
| export const startPo = async (poId: number) => { | |||
| const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(`${BASE_API_URL}/po/start/${poId}`, { | |||
| method: "POST", | |||
| body: JSON.stringify({ poId }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("po"); | |||
| return po | |||
| } | |||
| export const checkPolAndCompletePo = async (poId: number) => { | |||
| const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(`${BASE_API_URL}/po/check/${poId}`, { | |||
| method: "POST", | |||
| body: JSON.stringify({ poId }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("po"); | |||
| return po | |||
| } | |||
| export const fetchPoInClient = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const fetchPoListClient = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list?${queryString}`, { | |||
| method: 'GET', | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list`, { | |||
| method: 'GET', | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| } | |||
| }); | |||
| export const testing = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/testing?${queryString}`, { | |||
| method: 'GET', | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/testing`, { | |||
| method: 'GET', | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| } | |||
| }); | |||
| @@ -0,0 +1,81 @@ | |||
| import { cache } from "react"; | |||
| import "server-only"; | |||
| // import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| import { Uom } from "../settings/uom"; | |||
| import { RecordsRes } from "../utils"; | |||
| export interface PoResult { | |||
| id: number | |||
| code: string | |||
| orderDate: string | |||
| supplier: string | |||
| estimatedArrivalDate: string | |||
| completedDate: string | |||
| escalated: boolean | |||
| status: string | |||
| pol?: PurchaseOrderLine[] | |||
| } | |||
| export interface PurchaseOrderLine { | |||
| id: number | |||
| purchaseOrderId: number | |||
| itemId: number | |||
| itemNo: string | |||
| itemName: string | |||
| qty: number | |||
| processed: number | |||
| uom: Uom | |||
| price: number | |||
| status: string | |||
| stockInLine: StockInLine[] | |||
| } | |||
| export interface StockInLine { | |||
| id: number | |||
| stockInId: number | |||
| purchaseOrderId?: number | |||
| purchaseOrderLineId: number | |||
| itemId: number | |||
| itemNo: string | |||
| itemName: string | |||
| itemType: string | |||
| demandQty: number | |||
| acceptedQty: number | |||
| price: number | |||
| priceUnit: string | |||
| shelfLife?: number, | |||
| receiptDate?: string | |||
| productionDate?: string | |||
| expiryDate?: string | |||
| status: string | |||
| supplier: string | |||
| lotNo: string | |||
| poCode: string | |||
| uom: Uom | |||
| defaultWarehouseId: number // id for now | |||
| } | |||
| export const fetchPoList = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list?${queryString}`, { | |||
| method: 'GET', | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list`, { | |||
| method: 'GET', | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| } | |||
| }); | |||
| export const fetchPoWithStockInLines = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,32 @@ | |||
| import React, { useState } from "react"; | |||
| import { Card, CardHeader, CardContent, IconButton, Collapse } from "@mui/material"; | |||
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | |||
| import ExpandLessIcon from "@mui/icons-material/ExpandLess"; | |||
| interface CollapsibleCardProps { | |||
| title: string; | |||
| children: React.ReactNode; | |||
| defaultOpen?: boolean; | |||
| } | |||
| const CollapsibleCard: React.FC<CollapsibleCardProps> = ({ title, children, defaultOpen = true }) => { | |||
| const [open, setOpen] = useState(defaultOpen); | |||
| return ( | |||
| <Card> | |||
| <CardHeader | |||
| title={title} | |||
| action={ | |||
| <IconButton onClick={() => setOpen((v) => !v)}> | |||
| {open ? <ExpandLessIcon /> : <ExpandMoreIcon />} | |||
| </IconButton> | |||
| } | |||
| /> | |||
| <Collapse in={open}> | |||
| <CardContent>{children}</CardContent> | |||
| </Collapse> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default CollapsibleCard; | |||
| @@ -1,18 +0,0 @@ | |||
| "use client" | |||
| // npm install | |||
| interface Props { // params type | |||
| } | |||
| const DashboardLineChart: React.FC<Props> = ({ | |||
| // params | |||
| }) => { | |||
| return ( | |||
| <> | |||
| line chart | |||
| </> | |||
| ) | |||
| } | |||
| export default DashboardLineChart | |||
| @@ -6,9 +6,14 @@ import { TabsProps } from "@mui/material/Tabs"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { Card, CardContent, CardHeader, Grid } from "@mui/material"; | |||
| import DashboardProgressChart from "../DashboardProgressChart/DashboardProgressChart"; | |||
| import DashboardLineChart from "../DashboardLineChart/DashboardLineChart"; | |||
| import DashboardProgressChart from "./chart/DashboardProgressChart"; | |||
| import DashboardLineChart from "./chart/DashboardLineChart"; | |||
| import PendingInspectionChart from "./chart/PendingInspectionChart"; | |||
| import PendingStorageChart from "./chart/PendingStorageChart"; | |||
| import ApplicationCompletionChart from "./chart/ApplicationCompletionChart"; | |||
| import OrderCompletionChart from "./chart/OrderCompletionChart"; | |||
| import DashboardBox from "./DashboardBox"; | |||
| import CollapsibleCard from "./CollapsibleCard"; | |||
| type Props = {}; | |||
| const DashboardPage: React.FC<Props> = ({}) => { | |||
| @@ -17,30 +22,58 @@ const DashboardPage: React.FC<Props> = ({}) => { | |||
| return ( | |||
| <ThemeProvider theme={theme}> | |||
| <> | |||
| <Grid container> | |||
| <Grid item xs={12}> | |||
| <Card> | |||
| <CardHeader | |||
| title={t("Progress chart")} | |||
| /> | |||
| <CardContent> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <CollapsibleCard title={t("Progress chart")}> | |||
| <CardContent> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12} md={4}> | |||
| <DashboardProgressChart /> | |||
| </CardContent> | |||
| </Card> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <PendingInspectionChart /> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <PendingStorageChart /> | |||
| </Grid> | |||
| </Grid> | |||
| </CardContent> | |||
| </CollapsibleCard> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <CollapsibleCard title={t("Warehouse status")}> | |||
| <CardContent> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12} md={6}> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12} sm={6}> | |||
| <ApplicationCompletionChart /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={6}> | |||
| <OrderCompletionChart /> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Card> | |||
| <CardHeader | |||
| title={t("Line chart")} | |||
| /> | |||
| <CardContent> | |||
| <Grid item xs={12} md={6}> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12} sm={6}> | |||
| <DashboardBox title={t("Temperature status")} value="--" unit="°C" /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={6}> | |||
| <DashboardBox title={t("Humidity status")} value="--" unit="%" /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <DashboardLineChart /> | |||
| </CardContent> | |||
| </Card> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| </> | |||
| </CardContent> | |||
| </CollapsibleCard> | |||
| </Grid> | |||
| </Grid> | |||
| </ThemeProvider> | |||
| ); | |||
| }; | |||
| @@ -1,18 +0,0 @@ | |||
| "use client" | |||
| interface Props { // params type | |||
| } | |||
| const DashboardProgressChart: React.FC<Props> = ({ | |||
| // params | |||
| }) => { | |||
| return ( | |||
| <> | |||
| progress chart | |||
| </> | |||
| ) | |||
| } | |||
| export default DashboardProgressChart | |||
| @@ -0,0 +1,30 @@ | |||
| "use client" | |||
| import React from "react"; | |||
| interface DashboardBoxProps { | |||
| title: string; | |||
| value: string | number; | |||
| unit: string; | |||
| time?: string; | |||
| } | |||
| const DashboardBox: React.FC<DashboardBoxProps> = ({ title, value, unit, time }) => { | |||
| return ( | |||
| <div style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)", | |||
| marginBottom: 16 | |||
| }}> | |||
| <div style={{ fontWeight: 600, fontSize: 18, marginBottom: 12 }}>{title}</div> | |||
| <div style={{ fontSize: 24, color: "#1976d2", marginBottom: 8 }}> | |||
| <span style={{ fontSize: 32, fontWeight: 700 }}>{value}</span> {unit} | |||
| </div> | |||
| <div style={{ color: "#888" }}>{time || "NaN/NaN/NaN NaN:NaN:NaN"}</div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default DashboardBox; | |||
| @@ -0,0 +1,126 @@ | |||
| "use client" | |||
| import React, { useState } from "react"; | |||
| import dynamic from "next/dynamic"; | |||
| const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); | |||
| import { useTranslation } from "react-i18next"; | |||
| const ApplicationCompletionChart: React.FC = () => { | |||
| const { t } = useTranslation(); | |||
| const [tab, setTab] = useState(t("Raw material")); | |||
| const percent = 0; | |||
| const options = { | |||
| chart: { type: "donut" as const }, | |||
| labels: [], | |||
| colors: ["#42A5F5", "#e0e0e0"], | |||
| dataLabels: { | |||
| enabled: true, | |||
| formatter: () => `${percent}%`, | |||
| style: { fontSize: "32px", fontWeight: 600, color: "#1976d2" } | |||
| }, | |||
| legend: { show: false }, | |||
| plotOptions: { | |||
| pie: { | |||
| donut: { | |||
| size: "70%", | |||
| labels: { | |||
| show: false | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| stroke: { show: true, width: 2, colors: ['#fff'] } | |||
| }; | |||
| return ( | |||
| <div | |||
| style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)", | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| alignItems: "center", | |||
| }} | |||
| > | |||
| <div | |||
| style={{ | |||
| width: "100%", | |||
| fontWeight: 600, | |||
| fontSize: 18, | |||
| marginBottom: 12, | |||
| display: "flex", | |||
| alignItems: "center", | |||
| }} | |||
| > | |||
| <span | |||
| style={{ | |||
| flex: 1, | |||
| whiteSpace: "nowrap", | |||
| overflow: "hidden", | |||
| textOverflow: "ellipsis", | |||
| textAlign: "left", | |||
| }} | |||
| > | |||
| {t("Application completion")} | |||
| </span> | |||
| <div> | |||
| <button | |||
| style={{ | |||
| border: tab === t("Raw material") ? "1px solid #1976d2" : "1px solid #e0e0e0", | |||
| background: tab === t("Raw material") ? "#fff" : "#f5f5f5", | |||
| color: tab === t("Raw material") ? "#1976d2" : "#333", | |||
| borderRadius: 4, | |||
| padding: "2px 12px", | |||
| marginRight: 4, | |||
| cursor: "pointer" | |||
| }} | |||
| onClick={() => setTab(t("Raw material"))} | |||
| >{t("Raw material")}</button> | |||
| <button | |||
| style={{ | |||
| border: tab === t("Shipment") ? "1px solid #1976d2" : "1px solid #e0e0e0", | |||
| background: tab === t("Shipment") ? "#fff" : "#f5f5f5", | |||
| color: tab === t("Shipment") ? "#1976d2" : "#333", | |||
| borderRadius: 4, | |||
| padding: "2px 12px", | |||
| cursor: "pointer" | |||
| }} | |||
| onClick={() => setTab(t("Shipment"))} | |||
| >{t("Shipment")}</button> | |||
| </div> | |||
| </div> | |||
| <div | |||
| style={{ | |||
| width: "100%", | |||
| height: 320, | |||
| display: "flex", | |||
| alignItems: "center", | |||
| justifyContent: "center", | |||
| margin: "0 auto", | |||
| }} | |||
| > | |||
| <ApexCharts | |||
| options={options} | |||
| series={[0, 100]} | |||
| type="donut" | |||
| width="100%" | |||
| height={280} | |||
| /> | |||
| </div> | |||
| <div style={{ | |||
| marginTop: 16, | |||
| textAlign: "left", | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 8, | |||
| padding: 12, | |||
| background: "#fafafa" | |||
| }}> | |||
| <div>{t("Processed application")}: 0</div> | |||
| <div>{t("Pending application")}: 0</div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default ApplicationCompletionChart; | |||
| @@ -0,0 +1,74 @@ | |||
| "use client" | |||
| // npm install | |||
| import { Select, MenuItem, FormControl, InputLabel } from "@mui/material"; | |||
| import React,{useState} from "react"; | |||
| import dynamic from "next/dynamic"; | |||
| const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); | |||
| import { useTranslation } from "react-i18next"; | |||
| interface Props { // params type | |||
| } | |||
| const DashboardLineChart: React.FC = () => { | |||
| const { t } = useTranslation(); | |||
| const [warehouseType, setWarehouseType] = useState(t("Cold storage")); | |||
| const [timeRange, setTimeRange] = useState("6h"); | |||
| const options = { | |||
| chart: { type: "line" as const }, | |||
| xaxis: { | |||
| categories: ["10:00", "11:00", "12:00", "13:00", "14:00", "15:00"] | |||
| }, | |||
| yaxis: { | |||
| title: { text: t("Temperature") } | |||
| }, | |||
| stroke: { curve: "smooth" as const } | |||
| }; | |||
| const series = [ | |||
| { | |||
| name: "溫度", | |||
| data: [] | |||
| } | |||
| ]; | |||
| return ( | |||
| <div style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)", | |||
| marginBottom: 16, | |||
| width: "100%", | |||
| }}> | |||
| <div style={{ display: "flex", alignItems: "center", marginBottom: 12 }}> | |||
| <span style={{ fontWeight: 600, fontSize: 18, flex: 1 }}>{t("Warehouse temperature record")}</span> | |||
| <FormControl size="small" style={{ minWidth: 100, marginRight: 8 }}> | |||
| <InputLabel>{t("Warehouse type")}</InputLabel> | |||
| <Select | |||
| value={warehouseType} | |||
| label={t("Warehouse type")} | |||
| onChange={e => setWarehouseType(e.target.value)} | |||
| > | |||
| <MenuItem value={t("Cold storage")}>{t("Cold storage")}</MenuItem> | |||
| <MenuItem value={t("Normal temperature storage")}>{t("Normal temperature storage")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <FormControl size="small" style={{ minWidth: 100 }}> | |||
| <Select | |||
| value={timeRange} | |||
| onChange={e => setTimeRange(e.target.value)} | |||
| > | |||
| <MenuItem value="6h">{t("Last 6 hours")}</MenuItem> | |||
| <MenuItem value="24h">{t("Last 24 hours")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </div> | |||
| <ApexCharts options={options} series={series} type="line" width="100%" height={220} /> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default DashboardLineChart | |||
| @@ -0,0 +1,128 @@ | |||
| "use client" | |||
| import React, { useEffect, useState } from "react"; | |||
| import dynamic from "next/dynamic"; | |||
| import { PoResult } from "@/app/api/po"; | |||
| import { fetchPoListClient } from "@/app/api/po/actions"; | |||
| import { useTranslation } from "react-i18next"; | |||
| interface Props { // params type | |||
| po: PoResult[]; | |||
| } | |||
| const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); | |||
| const DashboardProgressChart: React.FC = () => { | |||
| const { t } = useTranslation("dashboard"); | |||
| const [series, setSeries] = useState<number[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [pending, setPending] = useState(0); | |||
| const [receiving, setReceiving] = useState(0); | |||
| useEffect(() => { | |||
| const fetchData = async () => { | |||
| const res = await fetchPoListClient(); | |||
| const records = res?.records || []; | |||
| const pendingCount = records.filter((r: any) => r.status === "pending").length; | |||
| const receivingCount = records.filter((r: any) => r.status === "receiving").length; | |||
| const totalCount = records.length; | |||
| setPending(pendingCount); | |||
| setReceiving(receivingCount); | |||
| setTotal(totalCount); | |||
| setSeries([pendingCount, receivingCount]); | |||
| }; | |||
| fetchData(); | |||
| }, []); | |||
| const options = { | |||
| chart: { | |||
| type: "donut" as const, | |||
| }, | |||
| labels: [t("pending"), t("receiving")], | |||
| dataLabels: { | |||
| formatter: (val: number) => `${val.toFixed(1)}%`, | |||
| dropShadow: { | |||
| enabled: false, | |||
| }, | |||
| style: { | |||
| fontSize: '18px', | |||
| fontWeight: 'bold', | |||
| }, | |||
| }, | |||
| legend: { | |||
| position: "bottom" as const, | |||
| fontSize: '16px', | |||
| markers: { | |||
| width: 16, | |||
| height: 16, | |||
| radius: 8, | |||
| }, | |||
| }, | |||
| colors: ["#A3C9F9", "#8DD7A9"], | |||
| plotOptions: { | |||
| pie: { | |||
| donut: { | |||
| size: "70%", | |||
| labels: { | |||
| show: true, | |||
| name: { show: false }, | |||
| value: { | |||
| show: true, | |||
| fontSize: "32px", | |||
| fontWeight: 600, | |||
| color: "#333", | |||
| formatter: function (val: string) { | |||
| return `${val}%`; | |||
| } | |||
| }, | |||
| total: { | |||
| show: false | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| stroke: { | |||
| show: true, | |||
| width: 2, | |||
| colors: ['#fff'] | |||
| }, | |||
| }; | |||
| return ( | |||
| <div style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| maxWidth: 400, | |||
| margin: "0 ", | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)" | |||
| }}> | |||
| <div style={{ fontWeight: 600, fontSize: 18, marginBottom: 12 }}>{t("採購訂單概覽")}</div> | |||
| <div style={{ width: 320, height: 320, display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto" }}> | |||
| {series.length > 0 ? ( | |||
| <ApexCharts | |||
| options={options} | |||
| series={series} | |||
| type="donut" | |||
| width={280} | |||
| height={280} | |||
| /> | |||
| ) : ( | |||
| <div style={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}> | |||
| 載入中... | |||
| </div> | |||
| )} | |||
| </div> | |||
| <div style={{ marginTop: 16, textAlign: "left" }}> | |||
| <div>{t("pending")}:{pending}</div> | |||
| <div>{t("receiving")}:{receiving}</div> | |||
| <div>{t("total")}:{total}</div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default DashboardProgressChart | |||
| @@ -0,0 +1,119 @@ | |||
| "use client" | |||
| import React, { useState } from "react"; | |||
| import dynamic from "next/dynamic"; | |||
| const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); | |||
| import { useTranslation } from "react-i18next"; | |||
| const OrderCompletionChart: React.FC = () => { | |||
| const { t } = useTranslation(); | |||
| const [tab, setTab] = useState(t("Raw material")); | |||
| const percent = 0; | |||
| const options = { | |||
| chart: { type: "donut" as const }, | |||
| labels: [], | |||
| colors: ["#42A5F5", "#e0e0e0"], | |||
| dataLabels: { | |||
| enabled: true, | |||
| formatter: () => `${percent}%`, | |||
| style: { fontSize: "32px", fontWeight: 600, color: "#1976d2" } | |||
| }, | |||
| legend: { show: false }, | |||
| plotOptions: { | |||
| pie: { | |||
| donut: { | |||
| size: "70%", | |||
| labels: { | |||
| show: false | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| stroke: { show: true, width: 2, colors: ['#fff'] } | |||
| }; | |||
| return ( | |||
| <div style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)" | |||
| }}> | |||
| <div style={{ fontWeight: 600, fontSize: 18, marginBottom: 12, display: "flex", alignItems: "center" }}> | |||
| <span style={{ | |||
| flex: 1, | |||
| whiteSpace: "nowrap", | |||
| overflow: "hidden", | |||
| textOverflow: "ellipsis" | |||
| }}>{t("Order completion")}</span> | |||
| <div> | |||
| <button | |||
| style={{ | |||
| border: tab === t("Raw material") ? "1px solid #1976d2" : "1px solid #e0e0e0", | |||
| background: tab === t("Raw material") ? "#fff" : "#f5f5f5", | |||
| color: tab === t("Raw material") ? "#1976d2" : "#333", | |||
| borderRadius: 4, | |||
| padding: "2px 12px", | |||
| marginRight: 4, | |||
| cursor: "pointer" | |||
| }} | |||
| onClick={() => setTab(t("Raw material"))} | |||
| >{t("Raw material")}</button> | |||
| <button | |||
| style={{ | |||
| border: tab === t("Consumable") ? "1px solid #1976d2" : "1px solid #e0e0e0", | |||
| background: tab === t("Consumable") ? "#fff" : "#f5f5f5", | |||
| color: tab === t("Consumable") ? "#1976d2" : "#333", | |||
| borderRadius: 4, | |||
| padding: "2px 12px", | |||
| marginRight: 4, | |||
| cursor: "pointer" | |||
| }} | |||
| onClick={() => setTab(t("Consumable"))} | |||
| >{t("Consumable")}</button> | |||
| <button | |||
| style={{ | |||
| border: tab === t("Shipment") ? "1px solid #1976d2" : "1px solid #e0e0e0", | |||
| background: tab === t("Shipment") ? "#fff" : "#f5f5f5", | |||
| color: tab === t("Shipment") ? "#1976d2" : "#333", | |||
| borderRadius: 4, | |||
| padding: "2px 12px", | |||
| cursor: "pointer" | |||
| }} | |||
| onClick={() => setTab(t("Shipment"))} | |||
| >{t("Shipment")}</button> | |||
| </div> | |||
| </div> | |||
| <div | |||
| style={{ | |||
| width: "100%", | |||
| height: 320, | |||
| display: "flex", | |||
| alignItems: "center", | |||
| justifyContent: "center", | |||
| margin: "0 auto", | |||
| }} | |||
| > | |||
| <ApexCharts | |||
| options={options} | |||
| series={[0, 100]} | |||
| type="donut" | |||
| width={280} | |||
| height={280} | |||
| /> | |||
| </div> | |||
| <div style={{ | |||
| marginTop: 16, | |||
| textAlign: "left", | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 8, | |||
| padding: 12, | |||
| background: "#fafafa" | |||
| }}> | |||
| <div>{t("Extracted order")}: 0</div> | |||
| <div>{t("Pending order")}: 0</div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default OrderCompletionChart; | |||
| @@ -0,0 +1,65 @@ | |||
| "use client" | |||
| import React from "react"; | |||
| import dynamic from "next/dynamic"; | |||
| const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); | |||
| import { useTranslation } from "react-i18next"; | |||
| const PendingInspectionChart: React.FC = () => { | |||
| const { t } = useTranslation("dashboard"); | |||
| const percent = 6.25; | |||
| const options = { | |||
| chart: { type: "donut" as const }, | |||
| labels: [t("pending inspection material"), t("inspected material")], | |||
| colors: ["#2196f3", "#e0e0e0"], | |||
| dataLabels: { enabled: false }, | |||
| legend: { position: "bottom" as const }, | |||
| plotOptions: { | |||
| pie: { | |||
| donut: { | |||
| size: "70%", | |||
| labels: { | |||
| show: true, | |||
| name: { show: false }, | |||
| value: { | |||
| show: true, | |||
| fontSize: "32px", | |||
| fontWeight: 600, | |||
| color: "#1976d2", | |||
| formatter: () => `${percent}%` | |||
| }, | |||
| total: { show: false } | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| stroke: { show: true, width: 2, colors: ['#fff'] } | |||
| }; | |||
| return ( | |||
| <div style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)" | |||
| }}> | |||
| <div style={{ fontWeight: 600, fontSize: 18, marginBottom: 12 }}>待品檢物料</div> | |||
| <div style={{ width: 320, height: 320, display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto" }}> | |||
| <ApexCharts | |||
| options={options} | |||
| series={[1, 15]} | |||
| type="donut" | |||
| width={280} | |||
| height={280} | |||
| /> | |||
| </div> | |||
| <div style={{ marginTop: 16, textAlign: "left" }}> | |||
| <div>{t("pending inspection material")}: 1</div> | |||
| <div>{t("total material")}: 16</div> | |||
| <div>{t("inspected material")}: 15</div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default PendingInspectionChart; | |||
| @@ -0,0 +1,65 @@ | |||
| "use client" | |||
| import React from "react"; | |||
| import dynamic from "next/dynamic"; | |||
| import { useTranslation } from "node_modules/react-i18next"; | |||
| const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); | |||
| const PendingStorageChart: React.FC = () => { | |||
| const { t } = useTranslation(); | |||
| const percent = 93.75; | |||
| const options = { | |||
| chart: { type: "donut" as const }, | |||
| labels: [t("Pending storage"), t("Total storage")], | |||
| colors: ["#1976d2", "#e0e0e0"], | |||
| dataLabels: { enabled: false }, | |||
| legend: { position: "bottom" as const }, | |||
| plotOptions: { | |||
| pie: { | |||
| donut: { | |||
| size: "70%", | |||
| labels: { | |||
| show: true, | |||
| name: { show: false }, | |||
| value: { | |||
| show: true, | |||
| fontSize: "32px", | |||
| fontWeight: 600, | |||
| color: "#1976d2", | |||
| formatter: () => `${percent}%` | |||
| }, | |||
| total: { show: false } | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| stroke: { show: true, width: 2, colors: ['#fff'] } | |||
| }; | |||
| return ( | |||
| <div style={{ | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 12, | |||
| padding: 24, | |||
| background: "#fff", | |||
| boxShadow: "0 2px 8px rgba(0,0,0,0.04)" | |||
| }}> | |||
| <div style={{ fontWeight: 600, fontSize: 18, marginBottom: 12 }}>{t("Pending storage")}</div> | |||
| <div style={{ width: 320, height: 320, display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto" }}> | |||
| <ApexCharts | |||
| options={options} | |||
| series={[15, 1]} | |||
| type="donut" | |||
| width={280} | |||
| height={280} | |||
| /> | |||
| </div> | |||
| <div style={{ marginTop: 16, textAlign: "left" }}> | |||
| <div> </div> {t("Pending storage")}: 15 | |||
| <div>{t("Total storage")}: 16</div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default PendingStorageChart; | |||
| @@ -16,7 +16,7 @@ interface Props { | |||
| const M18ImportDo: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { t } = useTranslation("m18ImportTesting") | |||
| const [isLoading, setIsLoading] = useState(false) | |||
| const { | |||
| control, | |||
| @@ -15,8 +15,7 @@ interface Props { | |||
| const M18ImportMasterData: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { t } = useTranslation("m18ImportTesting") | |||
| const [isLoading, setIsLoading] = useState(false) | |||
| const { | |||
| control, | |||
| @@ -16,7 +16,7 @@ interface Props { | |||
| const M18ImportPo: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation("settings") | |||
| const { t } = useTranslation("m18ImportTesting") | |||
| const [isLoading, setIsLoading] = useState(false) | |||
| const { | |||
| control, | |||
| @@ -16,7 +16,7 @@ interface Props { | |||
| const M18ImportPq: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { t } = useTranslation("m18ImportTesting") | |||
| const [isLoading, setIsLoading] = useState(false) | |||
| const { | |||
| control, | |||
| @@ -19,7 +19,7 @@ const M18ImportTesting: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { t } = useTranslation("m18ImportTesting") | |||
| const [isLoading, setIsLoading] = useState(false) | |||
| const [loadingType, setLoadingType] = useState<String | null>(null) | |||
| const formProps = useForm<M18ImportTestingForm>() | |||
| @@ -1,4 +1,33 @@ | |||
| { | |||
| "Dashboard": "資訊展示面板", | |||
| "Order status": "訂單狀態" | |||
| } | |||
| "Order status": "訂單狀態", | |||
| "pending": "未收貨", | |||
| "receiving": "收貨中", | |||
| "total": "未完成總數", | |||
| "Warehouse temperature record": "倉庫溫度記錄", | |||
| "Warehouse type": "倉庫類型", | |||
| "Last 6 hours": "過去6小時", | |||
| "Last 24 hours": "過去24小時", | |||
| "Cold storage": "冷藏倉", | |||
| "Normal temperature storage": "常溫倉", | |||
| "Temperature status": "溫度狀態", | |||
| "Humidity status": "濕度狀態", | |||
| "Warehouse status": "倉庫狀態", | |||
| "Progress chart": "進度圖表", | |||
| "Order completion": "訂單完成度", | |||
| "Raw material": "原料", | |||
| "Consumable": "消耗品", | |||
| "Shipment": "出貨", | |||
| "Extracted order": "已提取提料單", | |||
| "Pending order": "待提取提料單", | |||
| "Temperature": "溫度", | |||
| "Humidity": "濕度", | |||
| "Pending storage": "待入倉物料", | |||
| "Total storage": "已入倉物料", | |||
| "Application completion": "提料申請完成度", | |||
| "Processed application": "已處理提料申請", | |||
| "Pending application": "待處理提料申請", | |||
| "pending inspection material": "待品檢物料", | |||
| "inspected material": "已品檢物料", | |||
| "total material": "物料總數" | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| { | |||
| "Import Master Data": "匯入主資料", | |||
| "Modified Date From": "修改日期從", | |||
| "Modified Date From *": "修改日期從 *", | |||
| "Modified Date To": "修改日期到", | |||
| "Modified Date To *": "修改日期到 *", | |||
| "Import Purchase Order": "匯入採購單", | |||
| "Import Delivery Order": "匯入出貨單", | |||
| "Import Purchase Quotation": "匯入採購報價單", | |||
| "Import Po": "匯入採購單", | |||
| "Import Do": "匯入出貨單", | |||
| "Import Pq": "匯入採購報價單", | |||
| "Ready to import": "準備匯入", | |||
| "Status": "狀態" | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| { | |||
| "Mail": "郵件", | |||
| "Mail List": "郵件列表", | |||
| "Mail Name": "郵件名稱", | |||
| "Mail Description": "郵件描述", | |||
| "Mail Status": "郵件狀態", | |||
| "Mail Created At": "郵件創建時間", | |||
| "Mail Updated At": "郵件更新時間", | |||
| "Setting": "設定", | |||
| "Settings": "設定", | |||
| "Template": "模板", | |||
| "Code": "代碼", | |||
| "Description": "描述", | |||
| "Subject CHT": "主旨 (繁體中文)", | |||
| "Select Template (View By Code - Description)": "選擇模板 (代碼 - 描述)", | |||
| "MAIL.smtp.host": "SMTP 主機", | |||
| "MAIL.smtp.port": "SMTP 埠口", | |||
| "MAIL.smtp.username": "SMTP 使用者名稱", | |||
| "MAIL.smtp.password": "SMTP 密碼", | |||
| "MAIL.smtp.auth": "SMTP 認證", | |||
| "MAIL.smtp.ssl": "SMTP SSL" | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| { | |||
| "Qc Category": "QC 類別", | |||
| "Qc Category List": "QC 類別列表", | |||
| "Qc Category Name": "QC 類別名稱", | |||
| "Qc Category Description": "QC 類別描述", | |||
| "Qc Category Status": "QC 類別狀態", | |||
| "Qc Category Created At": "QC 類別創建時間", | |||
| "Qc Category Updated At": "QC 類別更新時間" | |||
| } | |||