| @@ -3,6 +3,7 @@ import { getServerI18n } from "@/i18n"; | |||||
| import { Stack } from "@mui/material"; | import { Stack } from "@mui/material"; | ||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import React, { Suspense } from "react"; | import React, { Suspense } from "react"; | ||||
| import { I18nProvider } from "@/i18n"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Import Testing" | title: "Import Testing" | ||||
| @@ -21,7 +22,9 @@ const M18ImportTestingPage: React.FC = async () => { | |||||
| > | > | ||||
| </Stack> | </Stack> | ||||
| <Suspense fallback={<M18ImportTesting.Loading />}> | <Suspense fallback={<M18ImportTesting.Loading />}> | ||||
| <M18ImportTesting /> | |||||
| <I18nProvider namespaces={["common", "m18ImportTesting"]}> | |||||
| <M18ImportTesting /> | |||||
| </I18nProvider> | |||||
| </Suspense> | </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 React, { useCallback, useEffect, useState } from "react"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { Card, CardContent, CardHeader, Grid } from "@mui/material"; | 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 = {}; | type Props = {}; | ||||
| const DashboardPage: React.FC<Props> = ({}) => { | const DashboardPage: React.FC<Props> = ({}) => { | ||||
| @@ -17,30 +22,58 @@ const DashboardPage: React.FC<Props> = ({}) => { | |||||
| return ( | return ( | ||||
| <ThemeProvider theme={theme}> | <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 /> | <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> | ||||
| <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 /> | <DashboardLineChart /> | ||||
| </CardContent> | |||||
| </Card> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| </> | |||||
| </CardContent> | |||||
| </CollapsibleCard> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </ThemeProvider> | </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 M18ImportDo: React.FC<Props> = ({ | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | |||||
| const { t } = useTranslation("m18ImportTesting") | |||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| const { | const { | ||||
| control, | control, | ||||
| @@ -15,8 +15,7 @@ interface Props { | |||||
| const M18ImportMasterData: React.FC<Props> = ({ | const M18ImportMasterData: React.FC<Props> = ({ | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | |||||
| const { t } = useTranslation("m18ImportTesting") | |||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| const { | const { | ||||
| control, | control, | ||||
| @@ -16,7 +16,7 @@ interface Props { | |||||
| const M18ImportPo: React.FC<Props> = ({ | const M18ImportPo: React.FC<Props> = ({ | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("settings") | |||||
| const { t } = useTranslation("m18ImportTesting") | |||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| const { | const { | ||||
| control, | control, | ||||
| @@ -16,7 +16,7 @@ interface Props { | |||||
| const M18ImportPq: React.FC<Props> = ({ | const M18ImportPq: React.FC<Props> = ({ | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | |||||
| const { t } = useTranslation("m18ImportTesting") | |||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| const { | const { | ||||
| control, | control, | ||||
| @@ -19,7 +19,7 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | |||||
| const { t } = useTranslation("m18ImportTesting") | |||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| const [loadingType, setLoadingType] = useState<String | null>(null) | const [loadingType, setLoadingType] = useState<String | null>(null) | ||||
| const formProps = useForm<M18ImportTestingForm>() | const formProps = useForm<M18ImportTestingForm>() | ||||
| @@ -1,4 +1,33 @@ | |||||
| { | { | ||||
| "Dashboard": "資訊展示面板", | "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 類別更新時間" | |||||
| } | |||||