@@ -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 類別更新時間" | |||
} |