diff --git a/.cursor/rules.md b/.cursor/rules.md
new file mode 100644
index 0000000..23b2996
--- /dev/null
+++ b/.cursor/rules.md
@@ -0,0 +1,91 @@
+# Project Guidelines - Always Follow These Rules
+
+## UI Standard (apply to all pages)
+
+All pages under `(main)` must share the same look and feel. Use this as the single source of truth for new and existing pages.
+
+### Stack & layout
+
+- **Styling:** Tailwind CSS for layout and utilities. MUI components are used with the project theme (primary blue, neutral borders) so they match the standard.
+- **Page wrapper:** Do **not** add a full-page wrapper with its own background or padding. The main layout (`src/app/(main)/layout.tsx`) already provides:
+ - Background: `bg-slate-50` (light), `dark:bg-slate-900` (dark)
+ - Padding: `p-4 sm:p-4 md:p-6 lg:p-8`
+- **Responsive:** Mobile-first; use breakpoints `sm`, `md`, `lg` (e.g. `flex-col sm:flex-row`, `p-4 md:p-6 lg:p-8`).
+- **Spacing:** Multiples of 4px only: `p-4`, `m-8`, `gap-2`, `gap-4`, `mb-4`, etc.
+
+### Theme & colors
+
+- **Default:** Light mode. Dark mode supported via `dark` class on `html`; use `dark:` Tailwind variants where needed.
+- **Primary:** `#3b82f6` (blue) — main actions, links, focus rings. In MUI this is `palette.primary.main`.
+- **Accent:** `#10b981` (emerald) — success, export, confirm actions.
+- **Design tokens** are in `src/app/global.css` (`:root` / `.dark`): `--primary`, `--accent`, `--background`, `--foreground`, `--card`, `--border`, `--muted`. Use these in custom CSS or Tailwind when you need to stay in sync.
+
+### Page structure (every page)
+
+1. **Page title bar (consistent across all pages):**
+ - Use the shared **PageTitleBar** component from `@/components/PageTitleBar` so every menu destination has the same title style.
+ - It renders a bar with: left primary accent (4px), white/card background, padding, and title typography (`text-xl` / `sm:text-2xl`, bold, slate-900 / dark slate-100).
+ - **Usage:** `` or with actions: `...} className="mb-4" />`.
+ - Do **not** put a bare `
` or `` as the main page heading; use PageTitleBar for consistency.
+
+2. **Content:** Fragments or divs with `space-y-4` (or `Stack spacing={2}` in MUI) between sections. No extra full-width background wrapper.
+
+### Search criteria
+
+- **When using the shared SearchBox component:** It already uses the standard card style. Ensure the parent page does not wrap it in another card.
+- **When building a custom search/query bar:** Use the shared class so it matches SearchBox:
+ - Wrapper: `className="app-search-criteria ..."` (plus layout classes like `flex flex-wrap items-center gap-2 p-4`).
+ - Label for “Search criteria” style: `className="app-search-criteria-label"` if you need a small uppercase label.
+- **Search button:** Primary action = blue (MUI `variant="contained"` `color="primary"`, or Tailwind `bg-blue-500 text-white`). Reset = outline with neutral border (e.g. MUI `variant="outlined"` with slate border, or Tailwind `border border-slate-300`).
+
+### Forms & inputs
+
+- **Standard look (enforced by MUI theme):** White background, border `#e2e8f0` (neutral 200), focus ring primary blue. Use MUI `TextField` / `FormControl` / date pickers as-is; the theme in `src/theme/devias-material-kit` already matches this.
+- **Tailwind-only forms (e.g. /ps):** Use the same tokens: `border border-slate-300`, `bg-white`, `focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20`, `text-slate-900`, `placeholder-slate-400`.
+
+### Buttons
+
+- **Primary action:** Blue filled — MUI `variant="contained"` `color="primary"` or Tailwind `bg-blue-500 text-white hover:bg-blue-600`.
+- **Secondary / cancel:** Outline, neutral — MUI `variant="outlined"` with border `#e2e8f0` / `#334155` text, or Tailwind `border border-slate-300 text-slate-700 hover:bg-slate-100`.
+- **Accent (e.g. export, success):** Green — MUI `color="success"` or Tailwind `bg-emerald-500` / `text-emerald-600` for outline.
+- **Spacing:** Use `gap-2` or `gap-4` between buttons; keep padding multiples of 4 (e.g. `px-4 py-2`).
+
+### Tables & grids
+
+- **Container:** Wrap tables/grids in a card-style container so they match across pages:
+ - MUI: `` (theme already uses 8px radius, neutral border).
+ - Tailwind: `rounded-lg border border-slate-200 bg-white shadow-sm`.
+- **Data grid (MUI X DataGrid):** Use `StyledDataGrid` from `@/components/StyledDataGrid`. It applies header bg neutral[50], header text neutral[700], cell padding and borders to match the standard.
+- **Table (MUI Table):** Use `SearchResults` when you have a paginated list; it uses `Paper variant="outlined"` and theme table styles (header bg, borders).
+- **Header row:** Background `bg-slate-50` / `neutral[50]`, text `text-slate-700` / `neutral[700]`, font-weight 600, padding `px-4 py-3` or theme default.
+- **Body rows:** Border `border-slate-200` / theme divider, hover `hover:bg-slate-50` / `action.hover`.
+
+### Cards & surfaces
+
+- **Standard card:** 8px radius, 1px border (`var(--border)` or `neutral[200]`), white background (`var(--card)`), light shadow (`0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)`). MUI `Card` and `Paper` are themed to match.
+- **Search-criteria card:** Use class `app-search-criteria` (left 4px primary border, same radius and shadow as above).
+
+### Menu bar & sidebar
+
+- **App bar (top):** White background, 1px bottom border (`palette.divider`), no heavy shadow (`elevation={0}`). Toolbar with consistent min-height and horizontal padding. Profile and title use `text.secondary` and font-weight 600.
+- **Sidebar (navigation drawer):** Same as cards: white background, 1px right border, light shadow. Logo area with padding and bottom border; nav list with 4px/8px margins, 8px border-radius on items. **Selected item:** primary light background tint, primary text/icon, font-weight 600. **Hover:** neutral hover background. Use `ListItemButton` with `mx: 1`, `minWidth: 40` on icons. Child items slightly smaller font (0.875rem).
+- **Profile dropdown:** Menu with 8px radius, 1px border (outlined Paper). Dense list, padding on header and items. Sign out as `MenuItem`.
+- **Selection logic:** Nav item is selected when `pathname === item.path` or `pathname.startsWith(item.path + "/")`. Parent with children expands on click; leaf items navigate via Link.
+- **Icons:** Use one icon per menu item that matches the action or section (e.g. Dashboard, LocalShipping for delivery, CalendarMonth for scheduling, Settings for settings). Prefer distinct MUI icons so items are easy to scan; avoid reusing the same icon for many items.
+
+### Reference implementations
+
+- **/ps** — Tailwind-only: query bar (`app-search-criteria`), buttons, table container, modals. Good reference for Tailwind patterns.
+- **/do** — SearchBox + StyledDataGrid inside Paper; page title on layout. Good reference for MUI + layout.
+- **/jo** — SearchBox + SearchResults (Paper-wrapped table); page title on layout. Same layout and search pattern as /do.
+
+When adding a **new page**, reuse the same structure: rely on the main layout for background/padding, use one optional standard `
`, then SearchBox (or `app-search-criteria` for custom bars), then Paper-wrapped grid/table or other content, with buttons and forms following the rules above.
+
+### Checklist for new pages
+
+- [ ] No extra full-page wrapper (background/padding come from main layout).
+- [ ] Page title: use `` (optional `actions`). Add `className="mb-4"` for spacing below.
+- [ ] Search/filter: use `SearchBox` or a div with `className="app-search-criteria"` for the bar.
+- [ ] Tables/grids: wrap in `Paper variant="outlined"` (MUI) or `rounded-lg border border-slate-200 bg-white shadow-sm` (Tailwind); use `StyledDataGrid` or `SearchResults` where applicable.
+- [ ] Buttons: primary = blue contained, secondary = outlined neutral, accent = green for success/export.
+- [ ] Spacing: multiples of 4px (`p-4`, `gap-2`, `mb-4`); responsive with `sm`/`md`/`lg`.
diff --git a/src/app/(main)/do/edit/page.tsx b/src/app/(main)/do/edit/page.tsx
index 857f3b1..a3200a5 100644
--- a/src/app/(main)/do/edit/page.tsx
+++ b/src/app/(main)/do/edit/page.tsx
@@ -1,38 +1,36 @@
import { SearchParams } from "@/app/utils/fetchUtil";
import DoDetail from "@/components/DoDetail/DoDetailWrapper";
+import PageTitleBar from "@/components/PageTitleBar";
import { I18nProvider, getServerI18n } from "@/i18n";
-import { Typography } from "@mui/material";
import { isArray } from "lodash";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { Suspense } from "react";
export const metadata: Metadata = {
- title: "Edit Delivery Order Detail"
-}
+ title: "Edit Delivery Order Detail",
+};
type Props = SearchParams;
const DoEdit: React.FC = async ({ searchParams }) => {
- const { t } = await getServerI18n("do");
- const id = searchParams["id"];
+ const { t } = await getServerI18n("do");
+ const id = searchParams["id"];
- if (!id || isArray(id) || !isFinite(parseInt(id))) {
- notFound();
- }
+ if (!id || isArray(id) || !isFinite(parseInt(id))) {
+ notFound();
+ }
- return (
- <>
-
- {t("Edit Delivery Order Detail")}
-
-
- }>
-
-
-
- >
- );
-}
+ return (
+ <>
+
+
+ }>
+
+
+
+ >
+ );
+};
export default DoEdit;
\ No newline at end of file
diff --git a/src/app/(main)/do/page.tsx b/src/app/(main)/do/page.tsx
index 8560496..e1ef75d 100644
--- a/src/app/(main)/do/page.tsx
+++ b/src/app/(main)/do/page.tsx
@@ -2,7 +2,7 @@
// import { getServerI18n } from "@/i18n"
import DoSearch from "../../../components/DoSearch";
import { getServerI18n } from "../../../i18n";
-import { Stack, Typography } from "@mui/material";
+import PageTitleBar from "@/components/PageTitleBar";
import { I18nProvider } from "@/i18n";
import { Metadata } from "next";
import { Suspense } from "react";
@@ -16,13 +16,7 @@ const DeliveryOrder: React.FC = async () => {
return (
<>
-
-
+ }>
diff --git a/src/app/(main)/jo/edit/page.tsx b/src/app/(main)/jo/edit/page.tsx
index 6868224..4a6b8ed 100644
--- a/src/app/(main)/jo/edit/page.tsx
+++ b/src/app/(main)/jo/edit/page.tsx
@@ -1,52 +1,51 @@
import { fetchJoDetail } from "@/app/api/jo";
import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil";
import JoSave from "@/components/JoSave";
+import PageTitleBar from "@/components/PageTitleBar";
import { I18nProvider, getServerI18n } from "@/i18n";
-import { Typography } from "@mui/material";
import { isArray } from "lodash";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { Suspense } from "react";
export const metadata: Metadata = {
- title: "Edit Job Order Detail"
-}
+ title: "Edit Job Order Detail",
+};
type Props = SearchParams;
const JoEdit: React.FC = async ({ searchParams }) => {
- const { t } = await getServerI18n("jo");
- const id = searchParams["id"];
-
- if (!id || isArray(id) || !isFinite(parseInt(id))) {
- notFound();
- }
-
- try {
- await fetchJoDetail(parseInt(id))
- } catch (e) {
-
- if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) {
- console.log("Job Order not found:", e);
- } else {
- console.error("Error fetching Job Order detail:", e);
- }
- notFound();
+ const { t } = await getServerI18n("jo");
+ const id = searchParams["id"];
+
+ if (!id || isArray(id) || !isFinite(parseInt(id))) {
+ notFound();
+ }
+
+ try {
+ await fetchJoDetail(parseInt(id));
+ } catch (e) {
+ if (
+ e instanceof ServerFetchError &&
+ (e.response?.status === 404 || e.response?.status === 400)
+ ) {
+ console.log("Job Order not found:", e);
+ } else {
+ console.error("Error fetching Job Order detail:", e);
}
-
-
- return (
- <>
-
- {t("Edit Job Order Detail")}
-
-
- }>
-
-
-
- >
- );
-}
+ notFound();
+ }
+
+ return (
+ <>
+
+
+ }>
+
+
+
+ >
+ );
+};
export default JoEdit;
\ No newline at end of file
diff --git a/src/app/(main)/jo/page.tsx b/src/app/(main)/jo/page.tsx
index 6e2f73a..d794c57 100644
--- a/src/app/(main)/jo/page.tsx
+++ b/src/app/(main)/jo/page.tsx
@@ -1,38 +1,29 @@
import { preloadBomCombo } from "@/app/api/bom";
import JoSearch from "@/components/JoSearch";
+import PageTitleBar from "@/components/PageTitleBar";
import { I18nProvider, getServerI18n } from "@/i18n";
-import { Stack, Typography } from "@mui/material";
import { Metadata } from "next";
import React, { Suspense } from "react";
export const metadata: Metadata = {
- title: "Job Order"
-}
+ title: "Job Order",
+};
-const jo: React.FC = async () => {
- const { t } = await getServerI18n("jo");
+const Jo: React.FC = async () => {
+ const { t } = await getServerI18n("jo");
- preloadBomCombo()
+ preloadBomCombo();
- return (
- <>
-
-
- {t("Search Job Order/ Create Job Order")}
-
-
- {/* TODO: Improve */}
- }>
-
-
-
- >
- )
-}
+ return (
+ <>
+
+
+ }>
+
+
+
+ >
+ );
+};
-export default jo;
\ No newline at end of file
+export default Jo;
\ No newline at end of file
diff --git a/src/app/(main)/jodetail/edit/page.tsx b/src/app/(main)/jodetail/edit/page.tsx
index 5172798..43a8027 100644
--- a/src/app/(main)/jodetail/edit/page.tsx
+++ b/src/app/(main)/jodetail/edit/page.tsx
@@ -1,8 +1,8 @@
import { fetchJoDetail } from "@/app/api/jo";
import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil";
import JoSave from "@/components/JoSave/JoSave";
+import PageTitleBar from "@/components/PageTitleBar";
import { I18nProvider, getServerI18n } from "@/i18n";
-import { Typography } from "@mui/material";
import { isArray } from "lodash";
import { Metadata } from "next";
import { notFound } from "next/navigation";
@@ -10,40 +10,41 @@ import { Suspense } from "react";
import GeneralLoading from "@/components/General/GeneralLoading";
export const metadata: Metadata = {
- title: "Edit Job Order Detail"
-}
+ title: "Edit Job Order Detail",
+};
type Props = SearchParams;
-const JoEdit: React.FC = async ({ searchParams }) => {
- const { t } = await getServerI18n("jo");
- const id = searchParams["id"];
+const JodetailEdit: React.FC = async ({ searchParams }) => {
+ const { t } = await getServerI18n("jo");
+ const id = searchParams["id"];
- if (!id || isArray(id) || !isFinite(parseInt(id))) {
- notFound();
- }
+ if (!id || isArray(id) || !isFinite(parseInt(id))) {
+ notFound();
+ }
- try {
- await fetchJoDetail(parseInt(id))
- } catch (e) {
- if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) {
- console.log(e)
- notFound();
- }
+ try {
+ await fetchJoDetail(parseInt(id));
+ } catch (e) {
+ if (
+ e instanceof ServerFetchError &&
+ (e.response?.status === 404 || e.response?.status === 400)
+ ) {
+ console.log(e);
+ notFound();
}
+ }
- return (
- <>
-
- {t("Edit Job Order Detail")}
-
-
- }>
-
-
-
- >
- );
-}
+ return (
+ <>
+
+
+ }>
+
+
+
+ >
+ );
+};
-export default JoEdit;
\ No newline at end of file
+export default JodetailEdit;
\ No newline at end of file
diff --git a/src/app/(main)/jodetail/page.tsx b/src/app/(main)/jodetail/page.tsx
index 37a61eb..7769148 100644
--- a/src/app/(main)/jodetail/page.tsx
+++ b/src/app/(main)/jodetail/page.tsx
@@ -1,39 +1,30 @@
import { preloadBomCombo } from "@/app/api/bom";
-import JodetailSearch from "@/components/Jodetail/JodetailSearch";
+import JodetailSearchWrapper from "@/components/Jodetail/FinishedGoodSearchWrapper";
+import GeneralLoading from "@/components/General/GeneralLoading";
+import PageTitleBar from "@/components/PageTitleBar";
import { I18nProvider, getServerI18n } from "@/i18n";
-import { Stack, Typography } from "@mui/material";
import { Metadata } from "next";
import React, { Suspense } from "react";
-import GeneralLoading from "@/components/General/GeneralLoading";
-import JodetailSearchWrapper from "@/components/Jodetail/FinishedGoodSearchWrapper";
+
export const metadata: Metadata = {
- title: "Job Order Pickexcution"
-}
+ title: "Job Order Pick Execution",
+};
-const jo: React.FC = async () => {
- const { t } = await getServerI18n("jo");
+const Jodetail: React.FC = async () => {
+ const { t } = await getServerI18n("jo");
- preloadBomCombo()
+ preloadBomCombo();
- return (
- <>
-
-
- {t("Job Order Pickexcution")}
-
-
-
- }>
-
-
-
- >
- )
-}
+ return (
+ <>
+
+
+ }>
+
+
+
+ >
+ );
+};
-export default jo;
\ No newline at end of file
+export default Jodetail;
\ No newline at end of file
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
index b8feb8f..45b6396 100644
--- a/src/app/(main)/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -49,8 +49,8 @@ export default async function MainLayout({
component="main"
sx={{
marginInlineStart: { xs: 0, xl: NAVIGATION_CONTENT_WIDTH },
- padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" },
}}
+ className="min-h-screen bg-slate-50 p-4 sm:p-4 md:p-6 lg:p-8 dark:bg-slate-900"
>
diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx
index b008657..de32437 100644
--- a/src/app/(main)/ps/page.tsx
+++ b/src/app/(main)/ps/page.tsx
@@ -1,85 +1,65 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
-import {
- Box, Paper, Typography, Button, Dialog, DialogTitle,
- DialogContent, DialogActions, TextField, Stack, Table,
- TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
- CircularProgress, Tooltip, DialogContentText
-} from "@mui/material";
-import {
- Search, Visibility, ListAlt, CalendarMonth,
- OnlinePrediction, FileDownload, SettingsEthernet
-} from "@mui/icons-material";
+import {
+ Search,
+ Eye,
+ ListOrdered,
+ LineChart,
+ Download,
+ Network,
+ Loader2,
+} from "lucide-react";
+import PageTitleBar from "@/components/PageTitleBar";
import dayjs from "dayjs";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
export default function ProductionSchedulePage() {
- // ── Main states ──
- const [searchDate, setSearchDate] = useState(dayjs().format('YYYY-MM-DD'));
+ const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD"));
const [schedules, setSchedules] = useState([]);
const [selectedLines, setSelectedLines] = useState([]);
const [isDetailOpen, setIsDetailOpen] = useState(false);
- const [selectedPs, setSelectedPs] = useState(null);
+ const [selectedPs, setSelectedPs] = useState(null);
const [loading, setLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
- // Forecast dialog
const [isForecastDialogOpen, setIsForecastDialogOpen] = useState(false);
- const [forecastStartDate, setForecastStartDate] = useState(dayjs().format('YYYY-MM-DD'));
- const [forecastDays, setForecastDays] = useState(7); // default 7 days
+ const [forecastStartDate, setForecastStartDate] = useState(
+ dayjs().format("YYYY-MM-DD")
+ );
+ const [forecastDays, setForecastDays] = useState(7);
- // Export dialog
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
- const [exportFromDate, setExportFromDate] = useState(dayjs().format('YYYY-MM-DD'));
+ const [exportFromDate, setExportFromDate] = useState(
+ dayjs().format("YYYY-MM-DD")
+ );
- // Auto-search on mount
useEffect(() => {
handleSearch();
}, []);
- // ── Formatters & Helpers ──
const formatBackendDate = (dateVal: any) => {
if (Array.isArray(dateVal)) {
const [year, month, day] = dateVal;
- return dayjs(new Date(year, month - 1, day)).format('DD MMM (dddd)');
+ return dayjs(new Date(year, month - 1, day)).format("DD MMM (dddd)");
}
- return dayjs(dateVal).format('DD MMM (dddd)');
+ return dayjs(dateVal).format("DD MMM (dddd)");
};
const formatNum = (num: any) => {
- return new Intl.NumberFormat('en-US').format(Number(num) || 0);
+ return new Intl.NumberFormat("en-US").format(Number(num) || 0);
};
- const isDateToday = useMemo(() => {
- if (!selectedPs?.produceAt) return false;
- const todayStr = dayjs().format('YYYY-MM-DD');
- let scheduleDateStr = "";
-
- if (Array.isArray(selectedPs.produceAt)) {
- const [y, m, d] = selectedPs.produceAt;
- scheduleDateStr = dayjs(new Date(y, m - 1, d)).format('YYYY-MM-DD');
- } else {
- scheduleDateStr = dayjs(selectedPs.produceAt).format('YYYY-MM-DD');
- }
-
- return todayStr === scheduleDateStr;
- }, [selectedPs]);
-
- // ── API Actions ──
const handleSearch = async () => {
setLoading(true);
-
try {
- const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, {
- method: 'GET',
- });
-
+ const response = await clientAuthFetch(
+ `${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`,
+ { method: "GET" }
+ );
if (response.status === 401 || response.status === 403) return;
-
const data = await response.json();
-
setSchedules(Array.isArray(data) ? data : []);
} catch (e) {
console.error("Search Error:", e);
@@ -89,28 +69,22 @@ export default function ProductionSchedulePage() {
};
const handleConfirmForecast = async () => {
- if (!forecastStartDate || forecastDays === '' || forecastDays < 1) {
+ if (!forecastStartDate || forecastDays === "" || forecastDays < 1) {
alert("Please enter a valid start date and number of days (≥1).");
return;
}
-
setLoading(true);
setIsForecastDialogOpen(false);
-
try {
const params = new URLSearchParams({
startDate: forecastStartDate,
days: forecastDays.toString(),
});
-
const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`;
-
- const response = await clientAuthFetch(url, { method: 'GET' });
-
+ const response = await clientAuthFetch(url, { method: "GET" });
if (response.status === 401 || response.status === 403) return;
-
if (response.ok) {
- await handleSearch(); // refresh list
+ await handleSearch();
alert("成功計算排期!");
} else {
const errorText = await response.text();
@@ -130,27 +104,21 @@ export default function ProductionSchedulePage() {
alert("Please select a from date.");
return;
}
-
setLoading(true);
setIsExportDialogOpen(false);
-
try {
- const params = new URLSearchParams({
- fromDate: exportFromDate,
- });
-
- const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, {
- method: 'GET',
- });
-
+ const params = new URLSearchParams({ fromDate: exportFromDate });
+ const response = await clientAuthFetch(
+ `${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`,
+ { method: "GET" }
+ );
if (response.status === 401 || response.status === 403) return;
if (!response.ok) throw new Error(`Export failed: ${response.status}`);
-
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
+ const a = document.createElement("a");
a.href = url;
- a.download = `production_schedule_from_${exportFromDate.replace(/-/g, '')}.xlsx`;
+ a.download = `production_schedule_from_${exportFromDate.replace(/-/g, "")}.xlsx`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
@@ -164,41 +132,24 @@ export default function ProductionSchedulePage() {
};
const handleViewDetail = async (ps: any) => {
- console.log("=== VIEW DETAIL CLICKED ===");
- console.log("Schedule ID:", ps?.id);
- console.log("Full ps object:", ps);
-
if (!ps?.id) {
alert("Cannot open details: missing schedule ID");
return;
}
-
setSelectedPs(ps);
setLoading(true);
-
try {
const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`;
-
- const response = await clientAuthFetch(url, { method: 'GET' });
-
+ const response = await clientAuthFetch(url, { method: "GET" });
if (response.status === 401 || response.status === 403) return;
-
if (!response.ok) {
const errorText = await response.text().catch(() => "(no text)");
- console.error("Server error response:", errorText);
alert(`Server error ${response.status}: ${errorText}`);
return;
}
-
const data = await response.json();
- console.log("Full received lines (JSON):", JSON.stringify(data, null, 2));
- console.log("Received data type:", typeof data);
- console.log("Received data:", data);
- console.log("Number of lines:", Array.isArray(data) ? data.length : "not an array");
-
setSelectedLines(Array.isArray(data) ? data : []);
setIsDetailOpen(true);
-
} catch (err) {
console.error("Fetch failed:", err);
alert("Network or fetch error – check console");
@@ -207,25 +158,21 @@ export default function ProductionSchedulePage() {
}
};
-
const handleAutoGenJob = async () => {
- //if (!isDateToday) return;
setIsGenerating(true);
try {
- const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: selectedPs.id })
- });
-
+ const response = await clientAuthFetch(
+ `${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ id: selectedPs.id }),
+ }
+ );
if (response.status === 401 || response.status === 403) return;
-
if (response.ok) {
const data = await response.json();
- const displayMessage = data.message || "Operation completed.";
-
- alert(displayMessage);
- //alert("Job Orders generated successfully!");
+ alert(data.message || "Operation completed.");
setIsDetailOpen(false);
} else {
alert("Failed to generate jobs.");
@@ -238,263 +185,380 @@ export default function ProductionSchedulePage() {
};
return (
-
-
- {/* Header */}
-
-
-
- 排程
-
-
-
- }
- onClick={() => setIsExportDialogOpen(true)}
- sx={{ fontWeight: 'bold' }}
- >
- 匯出計劃/物料需求Excel
-
- : }
- onClick={() => setIsForecastDialogOpen(true)}
- disabled={loading}
- sx={{ fontWeight: 'bold' }}
- >
- 預測排期
-
-
-
-
- {/* Query Bar – unchanged */}
-
-
+
+
+
+ >
+ }
+ className="mb-4"
+ />
+
+ {/* Query Bar */}
+