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