Преглед изворни кода

Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-frontend

# Conflicts:
#	src/components/JoSave/JoSave.tsx
#	src/i18n/zh/jo.json
#	src/i18n/zh/pickOrder.json
master
kelvin.yau пре 2 месеци
родитељ
комит
e1b8da2696
41 измењених фајлова са 10289 додато и 155 уклоњено
  1. +19
    -0
      src/app/(main)/jodetail/edit/not-found.tsx
  2. +49
    -0
      src/app/(main)/jodetail/edit/page.tsx
  3. +39
    -0
      src/app/(main)/jodetail/page.tsx
  4. +150
    -5
      src/app/api/jo/actions.ts
  5. +2
    -2
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  6. +99
    -102
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  7. +14
    -14
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  8. +15
    -7
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  9. +15
    -6
      src/components/JoSave/ActionButtons.tsx
  10. +32
    -5
      src/components/JoSave/JoSave.tsx
  11. +4
    -3
      src/components/JoSave/PickTable.tsx
  12. +231
    -0
      src/components/Jodetail/CombinedLotTable.tsx
  13. +321
    -0
      src/components/Jodetail/CreateForm.tsx
  14. +209
    -0
      src/components/Jodetail/CreatedItemsTable.tsx
  15. +179
    -0
      src/components/Jodetail/EscalationComponent.tsx
  16. +120
    -0
      src/components/Jodetail/FGPickOrderCard.tsx
  17. +556
    -0
      src/components/Jodetail/FInishedJobOrderRecord.tsx
  18. +26
    -0
      src/components/Jodetail/FinishedGoodSearchWrapper.tsx
  19. +79
    -0
      src/components/Jodetail/ItemSelect.tsx
  20. +1514
    -0
      src/components/Jodetail/JobPickExecution.tsx
  21. +383
    -0
      src/components/Jodetail/JobPickExecutionForm.tsx
  22. +1295
    -0
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  23. +1824
    -0
      src/components/Jodetail/Jobcreatitem.tsx
  24. +167
    -0
      src/components/Jodetail/Jodetail.tsx
  25. +443
    -0
      src/components/Jodetail/JodetailSearch.tsx
  26. +124
    -0
      src/components/Jodetail/LotConfirmationModal.tsx
  27. +527
    -0
      src/components/Jodetail/PutawayForm.tsx
  28. +395
    -0
      src/components/Jodetail/QCDatagrid.tsx
  29. +460
    -0
      src/components/Jodetail/QcFormVer2.tsx
  30. +78
    -0
      src/components/Jodetail/QcSelect.tsx
  31. +243
    -0
      src/components/Jodetail/SearchResultsTable.tsx
  32. +321
    -0
      src/components/Jodetail/StockInFormVer2.tsx
  33. +24
    -0
      src/components/Jodetail/TwoLineCell.tsx
  34. +73
    -0
      src/components/Jodetail/UomSelect.tsx
  35. +85
    -0
      src/components/Jodetail/VerticalSearchBox.tsx
  36. +78
    -0
      src/components/Jodetail/dummyQcTemplate.tsx
  37. +1
    -0
      src/components/Jodetail/index.ts
  38. +5
    -0
      src/components/NavigationContent/NavigationContent.tsx
  39. +15
    -1
      src/i18n/zh/common.json
  40. +58
    -0
      src/i18n/zh/jo.json
  41. +17
    -10
      src/i18n/zh/pickOrder.json

+ 19
- 0
src/app/(main)/jodetail/edit/not-found.tsx Прегледај датотеку

@@ -0,0 +1,19 @@
import { getServerI18n } from "@/i18n";
import { Stack, Typography, Link } from "@mui/material";
import NextLink from "next/link";

export default async function NotFound() {
const { t } = await getServerI18n("schedule", "common");

return (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">
{t("The job order page was not found!")}
</Typography>
<Link href="/settings/scheduling" component={NextLink} variant="body2">
{t("Return to all job orders")}
</Link>
</Stack>
);
}

+ 49
- 0
src/app/(main)/jodetail/edit/page.tsx Прегледај датотеку

@@ -0,0 +1,49 @@
import { fetchJoDetail } from "@/app/api/jo";
import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil";
import JoSave from "@/components/JoSave/JoSave";
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";
import GeneralLoading from "@/components/General/GeneralLoading";

export const metadata: Metadata = {
title: "Edit Job Order Detail"
}

type Props = SearchParams;

const JoEdit: React.FC<Props> = 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(e)
notFound();
}
}

return (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Edit Job Order Detail")}
</Typography>
<I18nProvider namespaces={["jo", "common"]}>
<Suspense fallback={<GeneralLoading />}>
<JoSave id={parseInt(id)} defaultValues={undefined} />
</Suspense>
</I18nProvider>
</>
);
}

export default JoEdit;

+ 39
- 0
src/app/(main)/jodetail/page.tsx Прегледај датотеку

@@ -0,0 +1,39 @@
import { preloadBomCombo } from "@/app/api/bom";
import JodetailSearch from "@/components/Jodetail/JodetailSearch";
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";

export const metadata: Metadata = {
title: "Job Order Pickexcution"
}

const jo: React.FC = async () => {
const { t } = await getServerI18n("jo");

preloadBomCombo()

return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Job Order Pickexcution")}
</Typography>
</Stack>
<I18nProvider namespaces={["jo", "common"]}>
<Suspense fallback={<GeneralLoading />}>
<JodetailSearch pickOrders={[]} />
</Suspense>
</I18nProvider>
</>
)
}

export default jo;

+ 150
- 5
src/app/api/jo/actions.ts Прегледај датотеку

@@ -38,15 +38,17 @@ export interface SearchJoResult {
status: JoStatus;
}

export interface ReleaseJoRequest {
// For Jo Button Actions
export interface CommonActionJoRequest {
id: number;
}

export interface ReleaseJoResponse {
export interface CommonActionJoResponse {
id: number;
entity: { status: JoStatus }
}

// For Jo Process
export interface IsOperatorExistResponse<T> {
id: number | null;
name: string;
@@ -76,7 +78,129 @@ export interface JobOrderDetail {
pickLines: any[];
status: string;
}
export interface UnassignedJobOrderPickOrder {
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
pickOrderTargetDate: string;
pickOrderStatus: string;
jobOrderId: number;
jobOrderCode: string;
jobOrderName: string;
reqQty: number;
uom: string;
planStart: string;
planEnd: string;
}

export interface AssignJobOrderResponse {
id: number | null;
code: string | null;
name: string | null;
type: string | null;
message: string | null;
errorPosition: string | null;
}
export const recordSecondScanIssue = cache(async (
pickOrderId: number,
itemId: number,
data: {
qty: number;
isMissing: boolean;
isBad: boolean;
reason: string;
createdBy: number;
}
) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
next: { tags: ["jo-second-scan"] },
},
);
});
export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`,
{
method: "POST",
next: { tags: ["jo-second-scan"] },
},
);
});

export const submitSecondScanQuantity = cache(async (
pickOrderId: number,
itemId: number,
data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string }
) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
next: { tags: ["jo-second-scan"] },
},
);
});
// 获取未分配的 Job Order pick orders
export const fetchUnassignedJobOrderPickOrders = cache(async () => {
return serverFetchJson<UnassignedJobOrderPickOrder[]>(
`${BASE_API_URL}/jo/unassigned-job-order-pick-orders`,
{
method: "GET",
next: { tags: ["jo-unassigned"] },
},
);
});

// 分配 Job Order pick order 给用户
export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => {
return serverFetchJson<AssignJobOrderResponse>(
`${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
}
);
};

// 获取 Job Order 分层数据
export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`,
{
method: "GET",
next: { tags: ["jo-hierarchical"] },
},
);
});

// 获取已完成的 Job Order pick orders
export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`,
{
method: "GET",
next: { tags: ["jo-completed"] },
},
);
});

// 获取已完成的 Job Order pick order records
export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => {
return serverFetchJson<any[]>(
`${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`,
{
method: "GET",
next: { tags: ["jo-records"] },
},
);
});
export const fetchJobOrderDetailByCode = cache(async (code: string) => {
return serverFetchJson<JobOrderDetail>(
`${BASE_API_URL}/jo/detailByCode/${code}`,
@@ -129,8 +253,17 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => {
return response
})

export const releaseJo = cache(async (data: ReleaseJoRequest) => {
return serverFetchJson<ReleaseJoResponse>(`${BASE_API_URL}/jo/release`,
export const releaseJo = cache(async (data: CommonActionJoRequest) => {
return serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/release`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
})
})

export const startJo = cache(async (data: CommonActionJoRequest) => {
return serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/start`,
{
method: "POST",
body: JSON.stringify(data),
@@ -139,10 +272,22 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => {
})

export const manualCreateJo = cache(async (data: SaveJo) => {
console.log(data)
return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/manualCreate`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" }
})
})

export const fetchCompletedJobOrderPickOrdersWithCompletedSecondScan = cache(async (userId: number) => {
return serverFetchJson<any[]>(`${BASE_API_URL}/jo/completed-job-order-pick-orders-with-completed-second-scan/${userId}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
})
})
export const fetchCompletedJobOrderPickOrderLotDetails = cache(async (pickOrderId: number) => {
return serverFetchJson<any[]>(`${BASE_API_URL}/jo/completed-job-order-pick-order-lot-details/${pickOrderId}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
})
})

+ 2
- 2
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Прегледај датотеку

@@ -690,8 +690,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Pick Order Detail")} iconPosition="end" />
<Tab label={t("Pick Execution Detail")} iconPosition="end" />
<Tab label={t("Pick Execution Record")} iconPosition="end" />
<Tab label={t("Finished Good Detail")} iconPosition="end" />
<Tab label={t("Finished Good Record")} iconPosition="end" />
</Tabs>
</Box>


+ 99
- 102
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx Прегледај датотеку

@@ -20,7 +20,7 @@ import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
import { fetchEscalationCombo } from "@/app/api/user/actions";
import { useRef } from "react";
interface LotPickData {
id: number;
lotId: number;
@@ -81,7 +81,6 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);

// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
@@ -92,7 +91,18 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0;
const req = selectedLot ? calculateRequiredQty(selectedLot) : 0;

const ap = Number(formData.actualPickQty) || 0;
const miss = Number(formData.missQty) || 0;
const bad = Number(formData.badItemQty) || 0;

// Max the user can type
const maxPick = Math.min(remaining, req);
const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad

const clamp0 = (v: any) => Math.max(0, Number(v) || 0);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
@@ -107,55 +117,49 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
fetchHandlers();
}, []);

// 初始化表单数据 - 每次打开时都重新初始化
const initKeyRef = useRef<string | null>(null);

useEffect(() => {
if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
const getSafeDate = (dateValue: any): string => {
if (!dateValue) return new Date().toISOString().split('T')[0];
try {
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
return new Date().toISOString().split('T')[0];
}
return date.toISOString().split('T')[0];
} catch {
return new Date().toISOString().split('T')[0];
}
};
if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return;

// 计算剩余可用数量
const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
console.log("=== PickExecutionForm Debug ===");
console.log("selectedLot:", selectedLot);
console.log("inQty:", selectedLot.inQty);
console.log("outQty:", selectedLot.outQty);
console.log("holdQty:", selectedLot.holdQty);
console.log("availableQty:", selectedLot.availableQty);
console.log("calculated remainingAvailableQty:", remainingAvailableQty);
console.log("=== End Debug ===");
setFormData({
pickOrderId: pickOrderId,
pickOrderCode: selectedPickOrderLine.pickOrderCode,
pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
pickExecutionDate: new Date().toISOString().split('T')[0],
pickOrderLineId: selectedPickOrderLine.id,
itemId: selectedPickOrderLine.itemId,
itemCode: selectedPickOrderLine.itemCode,
itemDescription: selectedPickOrderLine.itemName,
lotId: selectedLot.lotId,
lotNo: selectedLot.lotNo,
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: selectedLot.actualPickQty || 0,
missQty: 0,
badItemQty: 0, // 初始化为 0,用户需要手动输入
issueRemark: '',
pickerName: '',
handledBy: undefined,
});
}
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);
// Only initialize once per (pickOrderLineId + lotId) while dialog open
const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`;
if (initKeyRef.current === key) return;

const getSafeDate = (dateValue: any): string => {
if (!dateValue) return new Date().toISOString().split('T')[0];
try {
const d = new Date(dateValue);
return isNaN(d.getTime()) ? new Date().toISOString().split('T')[0] : d.toISOString().split('T')[0];
} catch {
return new Date().toISOString().split('T')[0];
}
};

setFormData({
pickOrderId: pickOrderId,
pickOrderCode: selectedPickOrderLine.pickOrderCode,
pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
pickExecutionDate: new Date().toISOString().split('T')[0],
pickOrderLineId: selectedPickOrderLine.id,
itemId: selectedPickOrderLine.itemId,
itemCode: selectedPickOrderLine.itemCode,
itemDescription: selectedPickOrderLine.itemName,
lotId: selectedLot.lotId,
lotNo: selectedLot.lotNo,
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: selectedLot.actualPickQty || 0,
missQty: 0,
badItemQty: 0,
issueRemark: '',
pickerName: '',
handledBy: undefined,
});

initKeyRef.current = key;
}, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]);
// Mutually exclusive inputs: picking vs reporting issues

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -168,30 +172,23 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
// ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
newErrors.actualPickQty = t('Qty is required');
}
// ✅ FIXED: Check if actual pick qty exceeds remaining available qty
if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
}
// ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty)
if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
const req = selectedLot?.requiredQty || 0;
const ap = formData.actualPickQty || 0;
const miss = formData.missQty || 0;
const bad = formData.badItemQty || 0;

if (ap < 0) newErrors.actualPickQty = t('Qty is required');
if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty');
if (miss < 0) newErrors.missQty = t('Invalid qty');
if (bad < 0) newErrors.badItemQty = t('Invalid qty');
if (ap + miss + bad > req) {
newErrors.actualPickQty = t('Total exceeds required qty');
newErrors.missQty = t('Total exceeds required qty');
}
// ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('At least one issue must be reported');
newErrors.badItemQty = t('At least one issue must be reported');
if (ap === 0 && miss === 0 && bad === 0) {
newErrors.actualPickQty = t('Enter pick qty or issue qty');
newErrors.missQty = t('Enter pick qty or issue qty');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@@ -266,42 +263,42 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('Actual Pick Qty')}
type="number"
value={formData.actualPickQty || 0}
onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
variant="outlined"
/>
<TextField
fullWidth
label={t('Actual Pick Qty')}
type="number"
value={formData.actualPickQty ?? ''}
onChange={(e) => handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('Missing item Qty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
error={!!errors.missQty}
// helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
variant="outlined"
/>
<TextField
fullWidth
label={t('Missing item Qty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
error={!!errors.missQty}
variant="outlined"
//disabled={(formData.actualPickQty || 0) > 0}
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('Bad Item Qty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
error={!!errors.badItemQty}
// helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
variant="outlined"
/>
<TextField
fullWidth
label={t('Bad Item Qty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
error={!!errors.badItemQty}
variant="outlined"
//disabled={(formData.actualPickQty || 0) > 0}
/>
</Grid>
{/* ✅ Show issue description and handler fields when bad items > 0 */}


+ 14
- 14
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx Прегледај датотеку

@@ -118,7 +118,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
// ✅ 新增:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrder[]>([]);
// ✅ 新增:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
@@ -358,10 +358,10 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {

{/* 加载状态 */}
{completedDoPickOrdersLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box>
{/* 结果统计 */}
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
@@ -370,12 +370,12 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {

{/* 列表 */}
{filteredDoPickOrders.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t("No completed DO pick orders found")}
</Typography>
</Box>
) : (
</Typography>
</Box>
) : (
<Stack spacing={2}>
{paginatedData.map((doPickOrder) => (
<Card key={doPickOrder.id}>
@@ -429,10 +429,10 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 25, 50]}
/>
)}
</Box>
)}
</Box>
)}
</Box>
)}
</Box>
</FormProvider>
);
};


+ 15
- 7
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Прегледај датотеку

@@ -586,12 +586,20 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setQrScanError(false);
setQrScanSuccess(false);
setQrScanInput('');
setIsManualScanning(false);
stopScan();
resetScan();
//setIsManualScanning(false);
//stopScan();
//resetScan();
setProcessedQrCodes(new Set());
setLastProcessedQr('');

setQrModalOpen(false);
setPickExecutionFormOpen(false);
if(selectedLotForQr?.stockOutLineId){
const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
qty: 0
});
}
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
@@ -709,9 +717,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setQrScanSuccess(true);
setQrScanError(false);
setQrScanInput(''); // Clear input after successful processing
setIsManualScanning(false);
stopScan();
resetScan();
//setIsManualScanning(false);
// stopScan();
// resetScan();
// ✅ Clear success state after a delay
//setTimeout(() => {


+ 15
- 6
src/components/JoSave/ActionButtons.tsx Прегледај датотеку

@@ -14,6 +14,7 @@ type Props = {
interface ErrorEntry {
qtyErr: boolean;
scanErr: boolean;
pickErr: boolean;
}

const ActionButtons: React.FC<Props> = ({
@@ -36,24 +37,31 @@ const ActionButtons: React.FC<Props> = ({
const errors: ErrorEntry = useMemo(() => {
let qtyErr = false;
let scanErr = false;
let pickErr = false

pickLines.forEach((line) => {
if (!qtyErr) {
const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0
qtyErr = pickedQty > 0 && pickedQty >= line.reqQty
qtyErr = pickedQty <= 0 || pickedQty < line.reqQty
}

if (!scanErr) {
scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false
}

if (!pickErr) {
pickErr = line.pickedLotNo === null
}
})

return {
qtyErr: qtyErr,
scanErr: scanErr
scanErr: scanErr,
pickErr: pickErr
}
}, [pickLines])
}, [pickLines, status])

console.log(pickLines)
return (
<Stack direction="row" justifyContent="flex-start" gap={1}>
{status === "planning" && (
@@ -71,12 +79,13 @@ const ActionButtons: React.FC<Props> = ({
variant="outlined"
startIcon={<PlayCircleFilledWhiteIcon />}
onClick={handleStart}
disabled={errors.qtyErr || errors.scanErr}
disabled={errors.qtyErr || errors.scanErr || errors.pickErr}
>
{t("Start Job Order")}
</Button>
{errors.scanErr && (<Typography variant="h3" color="error">{t("Please scan the item qr code.")}</Typography>)}
{errors.qtyErr && (<Typography variant="h3" color="error">{t("Please make sure the qty is enough.")}</Typography>)}
{errors.pickErr && (<Typography variant="h3" color="error">{t("Please make sure all required items are picked")}</Typography>)}
{errors.scanErr && (<Typography variant="h3" color="error">{t("Please scan the item qr code")}</Typography>)}
{errors.qtyErr && (<Typography variant="h3" color="error">{t("Please make sure the qty is enough")}</Typography>)}
</Box>
)}
</Stack>


+ 32
- 5
src/components/JoSave/JoSave.tsx Прегледај датотеку

@@ -8,13 +8,17 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } fro
import { Button, Stack, Typography } from "@mui/material";
import StartIcon from "@mui/icons-material/Start";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { releaseJo } from "@/app/api/jo/actions";
import { releaseJo, startJo } from "@/app/api/jo/actions";
import InfoCard from "./InfoCard";
import PickTable from "./PickTable";
import ActionButtons from "./ActionButtons";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
<<<<<<< HEAD
import JoRelease from "./JoRelease";
=======
import { submitDialog } from "../Swal/CustomAlerts";
>>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b

type Props = {
id?: number;
@@ -94,12 +98,18 @@ const JoSave: React.FC<Props> = ({
shouldValidate: true,
shouldDirty: true,
});

// Ask user and confirm to start JO
await submitDialog(() => handleStart(), t, {
title: t("Do you want to start job order"),
confirmButtonText: t("Start Job Order")
})
}
}
}
} finally {
scanner.resetScan()
setIsUploading(false)
scanner.resetScan()
setIsUploading(false)
}
}, [])

@@ -127,7 +137,6 @@ const JoSave: React.FC<Props> = ({
formProps.setValue("status", response.entity.status)
}
}

} catch (e) {
// backend error
setServerError(t("An error has occurred. Please try again later."));
@@ -138,7 +147,25 @@ const JoSave: React.FC<Props> = ({
}, [])

const handleStart = useCallback(async () => {
console.log("first")
try {
setIsUploading(true)
if (id) {
const response = await startJo({ id: id })
if (response) {
formProps.setValue("status", response.entity.status)

pickLines.map((line) => ({...line, status: "completed"}))

handleBack()
}
}
} catch (e) {
// backend error
setServerError(t("An error has occurred. Please try again later."));
console.log(e);
} finally {
setIsUploading(false)
}
}, [])

// --------------------------------------------- Form Submit --------------------------------------------- //


+ 4
- 3
src/components/JoSave/PickTable.tsx Прегледај датотеку

@@ -141,8 +141,8 @@ const PickTable: React.FC<Props> = ({
if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) {
return notPickedStatusColumn
}
const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned))
return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack>
const scanStatus = params.row.pickedLotNo.map((pln) => params.row.status === "completed" ? true : Boolean(pln.isScanned))
return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack key={`${params.id}-scan`} direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack>
},
},
{
@@ -213,10 +213,11 @@ const PickTable: React.FC<Props> = ({
align: "right",
headerAlign: "right",
renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => {
const status = Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))) || params.row.status === "completed"
return (
<>
{params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))}
{scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))}
{scanStatusColumn(status)}
</>
)
},


+ 231
- 0
src/components/Jodetail/CombinedLotTable.tsx Прегледај датотеку

@@ -0,0 +1,231 @@
"use client";

import {
Box,
Button,
CircularProgress,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
TablePagination,
} from "@mui/material";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";

interface CombinedLotTableProps {
combinedLotData: any[];
combinedDataLoading: boolean;
pickQtyData: Record<string, number>;
paginationController: {
pageNum: number;
pageSize: number;
};
onPickQtyChange: (lotKey: string, value: number | string) => void;
onSubmitPickQty: (lot: any) => void;
onRejectLot: (lot: any) => void;
onPageChange: (event: unknown, newPage: number) => void;
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

// ✅ Simple helper function to check if item is completed
const isItemCompleted = (lot: any) => {
const actualPickQty = Number(lot.actualPickQty) || 0;
const requiredQty = Number(lot.requiredQty) || 0;
return lot.stockOutLineStatus === 'completed' ||
(actualPickQty > 0 && requiredQty > 0 && actualPickQty >= requiredQty);
};

const isItemRejected = (lot: any) => {
return lot.stockOutLineStatus === 'rejected';
};

const CombinedLotTable: React.FC<CombinedLotTableProps> = ({
combinedLotData,
combinedDataLoading,
pickQtyData,
paginationController,
onPickQtyChange,
onSubmitPickQty,
onRejectLot,
onPageChange,
onPageSizeChange,
}) => {
const { t } = useTranslation("pickOrder");

// ✅ Paginated data
const paginatedLotData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return combinedLotData.slice(startIndex, endIndex);
}, [combinedLotData, paginationController]);

if (combinedDataLoading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress size={40} />
</Box>
);
}

return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
{/* <TableCell>{t("Expiry Date")}</TableCell> */}
<TableCell>{t("Location")}</TableCell>
<TableCell align="right">{t("Current Stock")}</TableCell>
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
<TableCell align="right">{t("Qty Already Picked")}</TableCell>
<TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell>
<TableCell>{t("Stock Unit")}</TableCell>
<TableCell align="center">{t("Submit")}</TableCell>
<TableCell align="center">{t("Reject")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedLotData.length === 0 ? (
<TableRow>
<TableCell colSpan={13} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedLotData.map((lot: any) => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const currentPickQty = pickQtyData[lotKey] ?? '';
const isCompleted = isItemCompleted(lot);
const isRejected = isItemRejected(lot);
// ✅ Green text color for completed items
const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit';
return (
<TableRow
key={lotKey}
sx={{
'&:hover': {
backgroundColor: 'action.hover',
},
}}
>
<TableCell sx={{ color: textColor }}>{lot.pickOrderCode}</TableCell>
<TableCell sx={{ color: textColor }}>{lot.itemCode}</TableCell>
<TableCell sx={{ color: textColor }}>{lot.itemName}</TableCell>
<TableCell sx={{ color: textColor }}>{lot.lotNo}</TableCell>
{/* <TableCell sx={{ color: textColor }}>
{lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A'}
</TableCell>
*/}
<TableCell sx={{ color: textColor }}>{lot.location}</TableCell>
<TableCell align="right" sx={{ color: textColor }}>{lot.availableQty}</TableCell>
<TableCell align="right" sx={{ color: textColor }}>{lot.requiredQty}</TableCell>
<TableCell align="right" sx={{ color: textColor }}>{lot.actualPickQty || 0}</TableCell>
<TableCell align="right">
<TextField
type="number"
value={currentPickQty}
onChange={(e) => {
onPickQtyChange(lotKey, e.target.value);
}}
onFocus={(e) => {
e.target.select();
}}
inputProps={{
min: 0,
max: lot.availableQty,
step: 0.01
}}
disabled={
isCompleted ||
isRejected ||
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable'
}
sx={{
width: '80px',
'& .MuiInputBase-input': {
textAlign: 'right',
}
}}
/>
</TableCell>
<TableCell sx={{ color: textColor }}>{lot.stockUnit}</TableCell>
<TableCell align="center">
<Button
variant="contained"
size="small"
disabled={isCompleted || isRejected || !currentPickQty || currentPickQty <= 0}
onClick={() => onSubmitPickQty(lot)}
sx={{
backgroundColor: isCompleted ? 'success.main' : 'primary.main',
color: 'white',
'&:disabled': {
backgroundColor: 'grey.300',
color: 'grey.500',
},
}}
>
{isCompleted ? t("Completed") : t("Submit")}
</Button>
</TableCell>
<TableCell align="center">
<Button
variant="outlined"
size="small"
color="error"
disabled={isCompleted || isRejected}
onClick={() => onRejectLot(lot)}
sx={{
'&:disabled': {
borderColor: 'grey.300',
color: 'grey.500',
},
}}
>
{t("Reject")}
</Button>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={combinedLotData.length}
page={paginationController.pageNum}
rowsPerPage={paginationController.pageSize}
onPageChange={onPageChange}
onRowsPerPageChange={onPageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);
};

export default CombinedLotTable;

+ 321
- 0
src/components/Jodetail/CreateForm.tsx Прегледај датотеку

@@ -0,0 +1,321 @@
"use client";

import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions";
import {
Autocomplete,
Box,
Card,
CardContent,
FormControl,
Grid,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { INPUT_DATE_FORMAT, stockInLineStatusMap } from "@/app/utils/formatUtil";
import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import axios from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import axiosInstance from "@/app/(main)/axios/axiosInstance";
import { SavePickOrderLineRequest, SavePickOrderRequest } from "@/app/api/pickOrder/actions";
import TwoLineCell from "../PoDetail/TwoLineCell";
import ItemSelect from "./ItemSelect";
import { ItemCombo } from "@/app/api/settings/item/actions";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";

interface Props {
items: ItemCombo[];
// disabled: boolean;
}
type EntryError =
| {
[field in keyof SavePickOrderLineRequest]?: string;
}
| undefined;

type PolRow = TableRow<Partial<SavePickOrderLineRequest>, EntryError>;
// fetchQcItemCheck
const CreateForm: React.FC<Props> = ({ items }) => {
const {
t,
i18n: { language },
} = useTranslation("pickOrder");
const apiRef = useGridApiRef();
const {
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
} = useFormContext<SavePickOrderRequest>();
console.log(defaultValues);
const targetDate = watch("targetDate");
//// validate form
// const accQty = watch("acceptedQty");
// const validateForm = useCallback(() => {
// console.log(accQty);
// if (accQty > itemDetail.acceptedQty) {
// setError("acceptedQty", {
// message: `${t("acceptedQty must not greater than")} ${
// itemDetail.acceptedQty
// }`,
// type: "required",
// });
// }
// if (accQty < 1) {
// setError("acceptedQty", {
// message: t("minimal value is 1"),
// type: "required",
// });
// }
// if (isNaN(accQty)) {
// setError("acceptedQty", {
// message: t("value must be a number"),
// type: "required",
// });
// }
// }, [accQty]);

// useEffect(() => {
// clearErrors();
// validateForm();
// }, [clearErrors, validateForm]);

const columns = useMemo<GridColDef[]>(
() => [
{
field: "itemId",
headerName: t("Item"),
// width: 100,
flex: 1,
editable: true,
valueFormatter(params) {
const row = params.id ? params.api.getRow<PolRow>(params.id) : null;
if (!row) {
return null;
}
const Item = items.find((q) => q.id === row.itemId);
return Item ? Item.label : t("Please select item");
},
renderCell(params: GridRenderCellParams<PolRow, number>) {
console.log(params.value);
return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
},
renderEditCell(params: GridRenderEditCellParams<PolRow, number>) {
const errorMessage =
params.row._error?.[params.field as keyof SavePickOrderLineRequest];
console.log(errorMessage);
const content = (
// <></>
<ItemSelect
allItems={items}
value={params.row.itemId}
onItemSelect={async (itemId, uom, uomId) => {
console.log(uom)
await params.api.setEditCellValue({
id: params.id,
field: "itemId",
value: itemId,
});
await params.api.setEditCellValue({
id: params.id,
field: "uom",
value: uom
})
await params.api.setEditCellValue({
id: params.id,
field: "uomId",
value: uomId
})
}}
/>
);
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
},
},
{
field: "qty",
headerName: t("qty"),
// width: 100,
flex: 1,
type: "number",
editable: true,
renderEditCell(params: GridRenderEditCellParams<PolRow>) {
const errorMessage =
params.row._error?.[params.field as keyof SavePickOrderLineRequest];
const content = <GridEditInputCell {...params} />;
return errorMessage ? (
<Tooltip title={t(errorMessage)}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
},
},
{
field: "uom",
headerName: t("uom"),
// width: 100,
flex: 1,
editable: true,
// renderEditCell(params: GridRenderEditCellParams<PolRow>) {
// console.log(params.row)
// const errorMessage =
// params.row._error?.[params.field as keyof SavePickOrderLineRequest];
// const content = <GridEditInputCell {...params} />;
// return errorMessage ? (
// <Tooltip title={t(errorMessage)}>
// <Box width="100%">{content}</Box>
// </Tooltip>
// ) : (
// content
// );
// }
}
],
[items, t],
);
/// validate datagrid
const validation = useCallback(
(newRow: GridRowModel<PolRow>): EntryError => {
const error: EntryError = {};
const { itemId, qty } = newRow;
if (!itemId || itemId <= 0) {
error["itemId"] = t("select qc");
}
if (!qty || qty <= 0) {
error["qty"] = t("enter a qty");
}
return Object.keys(error).length > 0 ? error : undefined;
},
[],
);

const typeList = [
{
type: "Consumable"
}
]

const onChange = useCallback(
(event: React.SyntheticEvent, newValue: {type: string}) => {
console.log(newValue);
setValue("type", newValue.type);
},
[setValue],
);

return (
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Pick Order Detail")}
</Typography>
</Grid>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={6} lg={6}>
<FormControl fullWidth>
<Autocomplete
disableClearable
fullWidth
getOptionLabel={(option) => option.type}
options={typeList}
onChange={onChange}
renderInput={(params) => <TextField {...params} label={t("type")}/>}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<Controller
control={control}
name="targetDate"
// rules={{ required: !Boolean(productionDate) }}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={{ width: "100%" }}
label={t("targetDate")}
value={targetDate ? dayjs(targetDate) : undefined}
onChange={(date) => {
console.log(date);
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("targetDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.targetDate?.message),
helperText: errors.targetDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
</Grid>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={12}>
<InputDataGrid<SavePickOrderRequest, SavePickOrderLineRequest, EntryError>
apiRef={apiRef}
checkboxSelection={false}
_formKey={"pickOrderLine"}
columns={columns}
validateRow={validation}
needAdd={true}
/>
</Grid>
</Grid>
</Grid>
);
};
export default CreateForm;

+ 209
- 0
src/components/Jodetail/CreatedItemsTable.tsx Прегледај датотеку

@@ -0,0 +1,209 @@
import React, { useCallback } from 'react';
import {
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Checkbox,
TextField,
TablePagination,
FormControl,
Select,
MenuItem,
} from '@mui/material';
import { useTranslation } from 'react-i18next';

interface CreatedItem {
itemId: number;
itemName: string;
itemCode: string;
qty: number;
uom: string;
uomId: number;
uomDesc: string;
isSelected: boolean;
currentStockBalance?: number;
targetDate?: string | null;
groupId?: number | null;
}

interface Group {
id: number;
name: string;
targetDate: string;
}

interface CreatedItemsTableProps {
items: CreatedItem[];
groups: Group[];
onItemSelect: (itemId: number, checked: boolean) => void;
onQtyChange: (itemId: number, qty: number) => void;
onGroupChange: (itemId: number, groupId: string) => void;
pageNum: number;
pageSize: number;
onPageChange: (event: unknown, newPage: number) => void;
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
items,
groups,
onItemSelect,
onQtyChange,
onGroupChange,
pageNum,
pageSize,
onPageChange,
onPageSizeChange,
}) => {
const { t } = useTranslation("pickOrder");

// Calculate pagination
const startIndex = (pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedItems = items.slice(startIndex, endIndex);

const handleQtyChange = useCallback((itemId: number, value: string) => {
const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1) {
onQtyChange(itemId, numValue);
}
}, [onQtyChange]);

return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}>
{t("Selected")}
</TableCell>
<TableCell>
{t("Item")}
</TableCell>
<TableCell>
{t("Group")}
</TableCell>
<TableCell align="right">
{t("Current Stock")}
</TableCell>
<TableCell align="right">
{t("Stock Unit")}
</TableCell>
<TableCell align="right">
{t("Order Quantity")}
</TableCell>
<TableCell align="right">
{t("Target Date")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedItems.length === 0 ? (
<TableRow>
<TableCell colSpan={12} align="center">
<Typography variant="body2" color="text.secondary">
{t("No created items")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedItems.map((item) => (
<TableRow key={item.itemId}>
<TableCell padding="checkbox">
<Checkbox
checked={item.isSelected}
onChange={(e) => onItemSelect(item.itemId, e.target.checked)}
/>
</TableCell>
<TableCell>
<Typography variant="body2">{item.itemName}</Typography>
<Typography variant="caption" color="textSecondary">
{item.itemCode}
</Typography>
</TableCell>
<TableCell>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={item.groupId?.toString() || ""}
onChange={(e) => onGroupChange(item.itemId, e.target.value)}
displayEmpty
>
<MenuItem value="">
<em>{t("No Group")}</em>
</MenuItem>
{groups.map((group) => (
<MenuItem key={group.id} value={group.id.toString()}>
{group.name}
</MenuItem>
))}
</Select>
</FormControl>
</TableCell>
<TableCell align="right">
<Typography
variant="body2"
color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"}
>
{item.currentStockBalance?.toLocaleString() || 0}
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="body2">{item.uomDesc}</Typography>
</TableCell>
<TableCell align="right">
<TextField
type="number"
size="small"
value={item.qty || ""}
onChange={(e) => handleQtyChange(item.itemId, e.target.value)}
inputProps={{
min: 1,
step: 1,
style: { textAlign: 'center' }
}}
sx={{
width: '80px',
'& .MuiInputBase-input': {
textAlign: 'center',
cursor: 'text'
}
}}
/>
</TableCell>
<TableCell align="right">
<Typography variant="body2">
{item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
</Typography>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={items.length}
page={(pageNum - 1)}
rowsPerPage={pageSize}
onPageChange={onPageChange}
onRowsPerPageChange={onPageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);
};

export default CreatedItemsTable;

+ 179
- 0
src/components/Jodetail/EscalationComponent.tsx Прегледај датотеку

@@ -0,0 +1,179 @@
import React, { useState, ChangeEvent, FormEvent, Dispatch } from 'react';
import {
Box,
Button,
Collapse,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Checkbox,
FormControlLabel,
Paper,
Typography,
RadioGroup,
Radio,
Stack,
Autocomplete,
} from '@mui/material';
import { SelectChangeEvent } from '@mui/material/Select';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { useTranslation } from 'react-i18next';

interface NameOption {
value: string;
label: string;
}

interface FormData {
name: string;
quantity: string;
message: string;
}

interface Props {
forSupervisor: boolean
isCollapsed: boolean
setIsCollapsed: Dispatch<React.SetStateAction<boolean>>
}
const EscalationComponent: React.FC<Props> = ({
forSupervisor,
isCollapsed,
setIsCollapsed
}) => {
const { t } = useTranslation("purchaseOrder");
const [formData, setFormData] = useState<FormData>({
name: '',
quantity: '',
message: '',
});

const nameOptions: NameOption[] = [
{ value: '', label: '請選擇姓名...' },
{ value: 'john', label: '張大明' },
{ value: 'jane', label: '李小美' },
{ value: 'mike', label: '王志強' },
{ value: 'sarah', label: '陳淑華' },
{ value: 'david', label: '林建國' },
];

const handleInputChange = (
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string>
): void => {
const { name, value } = event.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};

const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
console.log('表單已提交:', formData);
// 處理表單提交
};

const handleCollapseToggle = (e: ChangeEvent<HTMLInputElement>): void => {
setIsCollapsed(e.target.checked);
};

return (
// <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', p: 3 }}>
<>
<Paper>
{/* <Paper elevation={3} sx={{ mx: 'auto', p: 3 }}> */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={isCollapsed}
onChange={handleCollapseToggle}
color="primary"
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body1">上報結果</Typography>
{isCollapsed ? (
<ExpandLessIcon sx={{ ml: 1 }} />
) : (
<ExpandMoreIcon sx={{ ml: 1 }} />
)}
</Box>
}
/>
</Box>
<Collapse in={isCollapsed}>
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{forSupervisor ? (
<FormControl>
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
defaultValue="pass"
name="radio-buttons-group"
>
<FormControlLabel value="pass" control={<Radio />} label="合格" />
<FormControlLabel value="fail" control={<Radio />} label="不合格" />
</RadioGroup>
</FormControl>
): undefined}
<FormControl fullWidth>
<select
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
>
{nameOptions.map((option: NameOption) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</FormControl>
<TextField
fullWidth
id="quantity"
name="quantity"
label="數量"
type="number"
value={formData.quantity}
onChange={handleInputChange}
InputProps={{ inputProps: { min: 1 } }}
placeholder="請輸入數量"
/>

<TextField
fullWidth
id="message"
name="message"
label="備註"
multiline
rows={4}
value={formData.message}
onChange={handleInputChange}
placeholder="請輸入您的備註"
/>

<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
type="submit"
variant="contained"
color="primary"
>
{t("update qc info")}
</Button>
</Stack>
</Box>
</Collapse>
</Paper>
</>
);
}

export default EscalationComponent;

+ 120
- 0
src/components/Jodetail/FGPickOrderCard.tsx Прегледај датотеку

@@ -0,0 +1,120 @@
"use client";

import { FGPickOrderResponse } from "@/app/api/pickOrder/actions";
import { Box, Card, CardContent, Grid, Stack, TextField, Button } from "@mui/material";
import { useTranslation } from "react-i18next";
import QrCodeIcon from '@mui/icons-material/QrCode';

type Props = {
fgOrder: FGPickOrderResponse;
onQrCodeClick: (pickOrderId: number) => void;
};

const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => {
const { t } = useTranslation("pickOrder");

return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Delivery Code")}
fullWidth
disabled={true}
value={fgOrder.deliveryNo}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Pick Order Code")}
fullWidth
disabled={true}
value={fgOrder.pickOrderCode}
//helperText={fgOrder.pickOrderConsoCode}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Shop PO Code")}
fullWidth
disabled={true}
value={fgOrder.shopPoNo}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Store ID")}
fullWidth
disabled={true}
value={fgOrder.storeId}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Shop ID")}
fullWidth
disabled={true}
value={fgOrder.shopCode}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Shop Name")}
fullWidth
disabled={true}
value={fgOrder.shopName}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Delivery Date")}
fullWidth
disabled={true}
value={new Date(fgOrder.deliveryDate).toLocaleDateString()}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Shop Address")}
fullWidth
disabled={true}
value={fgOrder.shopAddress}
multiline
rows={2}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Departure Time")}
fullWidth
disabled={true}
value={fgOrder.DepartureTime}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Truck No.")}
fullWidth
disabled={true}
value={fgOrder.truckNo}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Ticket No.")}
fullWidth
disabled={true}
value={fgOrder.ticketNo}
/>
</Grid>

</Grid>
</Box>
</CardContent>
</Card>
);
};

export default FGPickOrderCard;

+ 556
- 0
src/components/Jodetail/FInishedJobOrderRecord.tsx Прегледај датотеку

@@ -0,0 +1,556 @@
"use client";

import {
Box,
Button,
Stack,
TextField,
Typography,
Alert,
CircularProgress,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
TablePagination,
Modal,
Card,
CardContent,
CardActions,
Chip,
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox, // ✅ Add Checkbox import
} from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import {
fetchCompletedJobOrderPickOrdersWithCompletedSecondScan,
fetchCompletedJobOrderPickOrderLotDetails
} from "@/app/api/jo/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
FormProvider,
useForm,
} from "react-hook-form";
import SearchBox, { Criterion } from "../SearchBox";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";

interface Props {
filterArgs: Record<string, any>;
}

// ✅ 修改:已完成的 Job Order Pick Order 接口
interface CompletedJobOrderPickOrder {
id: number;
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
pickOrderTargetDate: string;
pickOrderStatus: string;
completedDate: string;
jobOrderId: number;
jobOrderCode: string;
jobOrderName: string;
reqQty: number;
uom: string;
planStart: string;
planEnd: string;
secondScanCompleted: boolean;
totalItems: number;
completedItems: number;
}

// ✅ 新增:Lot 详情接口
interface LotDetail {
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
availableQty: number;
requiredQty: number;
actualPickQty: number;
processingStatus: string;
lotAvailability: string;
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
pickOrderLineId: number;
stockOutLineId: number;
stockOutLineStatus: string;
routerIndex: number;
routerArea: string;
routerRoute: string;
uomShortDesc: string;
secondQrScanStatus: string;
itemId: number;
itemCode: string;
itemName: string;
uomCode: string;
uomDesc: string;
}

const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("jo");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// ✅ 修改:已完成 Job Order Pick Orders 状态
const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false);
// ✅ 修改:详情视图状态
const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null);
const [showDetailView, setShowDetailView] = useState(false);
const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]);
const [detailLotDataLoading, setDetailLotDataLoading] = useState(false);
// ✅ 修改:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
// ✅ 修改:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
pageSize: 10,
});

const formProps = useForm();
const errors = formProps.formState.errors;

// ✅ 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders
const fetchCompletedJobOrderPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
setCompletedJobOrderPickOrdersLoading(true);
try {
console.log("🔍 Fetching completed Job Order pick orders...");
const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersWithCompletedSecondScan(currentUserId);
setCompletedJobOrderPickOrders(completedJobOrderPickOrders);
setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
console.log("✅ Fetched completed Job Order pick orders:", completedJobOrderPickOrders);
} catch (error) {
console.error("❌ Error fetching completed Job Order pick orders:", error);
setCompletedJobOrderPickOrders([]);
setFilteredJobOrderPickOrders([]);
} finally {
setCompletedJobOrderPickOrdersLoading(false);
}
}, [currentUserId]);

// ✅ 新增:获取 lot 详情数据
const fetchLotDetailsData = useCallback(async (pickOrderId: number) => {
setDetailLotDataLoading(true);
try {
console.log("🔍 Fetching lot details for pick order:", pickOrderId);
const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId);
setDetailLotData(lotDetails);
console.log("✅ Fetched lot details:", lotDetails);
} catch (error) {
console.error("❌ Error fetching lot details:", error);
setDetailLotData([]);
} finally {
setDetailLotDataLoading(false);
}
}, []);

// ✅ 修改:初始化时获取数据
useEffect(() => {
if (currentUserId) {
fetchCompletedJobOrderPickOrdersData();
}
}, [currentUserId, fetchCompletedJobOrderPickOrdersData]);

// ✅ 修改:搜索功能
const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);

const filtered = completedJobOrderPickOrders.filter((pickOrder) => {
const pickOrderCodeMatch = !query.pickOrderCode ||
pickOrder.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
const jobOrderCodeMatch = !query.jobOrderCode ||
pickOrder.jobOrderCode?.toLowerCase().includes((query.jobOrderCode || "").toLowerCase());
const jobOrderNameMatch = !query.jobOrderName ||
pickOrder.jobOrderName?.toLowerCase().includes((query.jobOrderName || "").toLowerCase());
return pickOrderCodeMatch && jobOrderCodeMatch && jobOrderNameMatch;
});
setFilteredJobOrderPickOrders(filtered);
console.log("Filtered Job Order pick orders count:", filtered.length);
}, [completedJobOrderPickOrders]);

// ✅ 修改:重置搜索
const handleSearchReset = useCallback(() => {
setSearchQuery({});
setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
}, [completedJobOrderPickOrders]);

// ✅ 修改:分页功能
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({
...prev,
pageNum: newPage,
}));
}, []);

const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10);
setPaginationController({
pageNum: 0,
pageSize: newPageSize,
});
}, []);

// ✅ 修改:分页数据
const paginatedData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return filteredJobOrderPickOrders.slice(startIndex, endIndex);
}, [filteredJobOrderPickOrders, paginationController]);

// ✅ 修改:搜索条件
const searchCriteria: Criterion<any>[] = [
{
label: t("Pick Order Code"),
paramName: "pickOrderCode",
type: "text",
},
{
label: t("Job Order Code"),
paramName: "jobOrderCode",
type: "text",
},
{
label: t("Job Order Item Name"),
paramName: "jobOrderName",
type: "text",
}
];

// ✅ 修改:详情点击处理
const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => {
setSelectedJobOrderPickOrder(jobOrderPickOrder);
setShowDetailView(true);
// ✅ 获取 lot 详情数据
await fetchLotDetailsData(jobOrderPickOrder.pickOrderId);
// ✅ 触发打印按钮状态更新 - 基于详情数据
const allCompleted = jobOrderPickOrder.secondScanCompleted;
// ✅ 发送事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件
}
}));
}, [fetchLotDetailsData]);

// ✅ 修改:返回列表视图
const handleBackToList = useCallback(() => {
setShowDetailView(false);
setSelectedJobOrderPickOrder(null);
setDetailLotData([]);
// ✅ 返回列表时禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
tabIndex: 2
}
}));
}, []);

// ✅ 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
if (showDetailView && selectedJobOrderPickOrder) {
return (
<FormProvider {...formProps}>
<Box>
{/* 返回按钮和标题 */}
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Button variant="outlined" onClick={handleBackToList}>
{t("Back to List")}
</Button>
<Typography variant="h6">
{t("Job Order Pick Order Details")}: {selectedJobOrderPickOrder.pickOrderCode}
</Typography>
</Box>

{/* Job Order 信息卡片 */}
<Card sx={{ mb: 2 }}>
<CardContent>
<Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
<Typography variant="subtitle1">
<strong>{t("Pick Order Code")}:</strong> {selectedJobOrderPickOrder.pickOrderCode}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Job Order Code")}:</strong> {selectedJobOrderPickOrder.jobOrderCode}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Job Order Item Name")}:</strong> {selectedJobOrderPickOrder.jobOrderName}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Target Date")}:</strong> {selectedJobOrderPickOrder.pickOrderTargetDate}
</Typography>
</Stack>
<Stack direction="row" spacing={4} useFlexGap flexWrap="wrap" sx={{ mt: 2 }}>
<Typography variant="subtitle1">
<strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom}
</Typography>
</Stack>
</CardContent>
</Card>

{/* ✅ 修改:Lot 详情表格 - 添加复选框列 */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{t("Lot Details")}
</Typography>
{detailLotDataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Index")}</TableCell>
<TableCell>{t("Route")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell>{t("Location")}</TableCell>
<TableCell align="right">{t("Required Qty")}</TableCell>
<TableCell align="right">{t("Actual Pick Qty")}</TableCell>
<TableCell align="center">{t("Processing Status")}</TableCell>
<TableCell align="center">{t("Second Scan Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{detailLotData.length === 0 ? (
<TableRow>
<TableCell colSpan={10} align="center"> {/* ✅ 恢复原来的 colSpan */}
<Typography variant="body2" color="text.secondary">
{t("No lot details available")}
</Typography>
</TableCell>
</TableRow>
) : (
detailLotData.map((lot, index) => (
<TableRow key={lot.lotId}>
<TableCell>
<Typography variant="body2" fontWeight="bold">
{index + 1}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{lot.routerRoute || '-'}
</Typography>
</TableCell>
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{lot.itemName}</TableCell>
<TableCell>{lot.lotNo}</TableCell>
<TableCell>{lot.location}</TableCell>
<TableCell align="right">
{lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc})
</TableCell>
<TableCell align="right">
{lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc})
</TableCell>
{/* ✅ 修改:Processing Status 使用复选框 */}
<TableCell align="center">
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%'
}}>
<Checkbox
checked={lot.processingStatus === 'completed'}
disabled={true}
readOnly={true}
size="large"
sx={{
color: lot.processingStatus === 'completed' ? 'success.main' : 'grey.400',
'&.Mui-checked': {
color: 'success.main',
},
transform: 'scale(1.3)',
'& .MuiSvgIcon-root': {
fontSize: '1.5rem',
}
}}
/>
</Box>
</TableCell>
{/* ✅ 修改:Second Scan Status 使用复选框 */}
<TableCell align="center">
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%'
}}>
<Checkbox
checked={lot.secondQrScanStatus === 'completed'}
disabled={true}
readOnly={true}
size="large"
sx={{
color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400',
'&.Mui-checked': {
color: 'success.main',
},
transform: 'scale(1.3)',
'& .MuiSvgIcon-root': {
fontSize: '1.5rem',
}
}}
/>
</Box>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</CardContent>
</Card>
</Box>
</FormProvider>
);
}

// ✅ 修改:默认列表视图
return (
<FormProvider {...formProps}>
<Box>
{/* 搜索框 */}
<Box sx={{ mb: 2 }}>
<SearchBox
criteria={searchCriteria}
onSearch={handleSearch}
onReset={handleSearchReset}
/>
</Box>

{/* 加载状态 */}
{completedJobOrderPickOrdersLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box>
{/* 结果统计 */}
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")}
</Typography>

{/* 列表 */}
{filteredJobOrderPickOrders.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t("No completed Job Order pick orders with matching found")}
</Typography>
</Box>
) : (
<Stack spacing={2}>
{paginatedData.map((jobOrderPickOrder) => (
<Card key={jobOrderPickOrder.id}>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="h6">
{jobOrderPickOrder.pickOrderCode}
</Typography>
<Typography variant="body2" color="text.secondary">
{jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.jobOrderCode}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate}
</Typography>
</Box>
<Box>
<Chip
label={jobOrderPickOrder.pickOrderStatus}
color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'}
size="small"
sx={{ mb: 1 }}
/>
<Typography variant="body2" color="text.secondary">
{jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")}
</Typography>
<Chip
label={jobOrderPickOrder.secondScanCompleted ? t("Second Scan Completed") : t("Second Scan Pending")}
color={jobOrderPickOrder.secondScanCompleted ? 'success' : 'warning'}
size="small"
sx={{ mt: 1 }}
/>
</Box>
</Stack>
</CardContent>
<CardActions>
<Button
variant="outlined"
onClick={() => handleDetailClick(jobOrderPickOrder)}
>
{t("View Details")}
</Button>
</CardActions>
</Card>
))}
</Stack>
)}

{/* 分页 */}
{filteredJobOrderPickOrders.length > 0 && (
<TablePagination
component="div"
count={filteredJobOrderPickOrders.length}
page={paginationController.pageNum}
rowsPerPage={paginationController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 25, 50]}
/>
)}
</Box>
)}
</Box>
</FormProvider>
);
};

export default FInishedJobOrderRecord;

+ 26
- 0
src/components/Jodetail/FinishedGoodSearchWrapper.tsx Прегледај датотеку

@@ -0,0 +1,26 @@
import { fetchPickOrders } from "@/app/api/pickOrder";
import GeneralLoading from "../General/GeneralLoading";
import PickOrderSearch from "./FinishedGoodSearchWrapper";

interface SubComponents {
Loading: typeof GeneralLoading;
}

const FinishedGoodSearchWrapper: React.FC & SubComponents = async () => {
const [pickOrders] = await Promise.all([
fetchPickOrders({
code: undefined,
targetDateFrom: undefined,
targetDateTo: undefined,
type: undefined,
status: undefined,
itemName: undefined,
}),
]);

return <FinishedGoodSearchWrapper pickOrders={pickOrders} />;
};

FinishedGoodSearchWrapper.Loading = GeneralLoading;

export default FinishedGoodSearchWrapper;

+ 79
- 0
src/components/Jodetail/ItemSelect.tsx Прегледај датотеку

@@ -0,0 +1,79 @@

import { ItemCombo } from "@/app/api/settings/item/actions";
import { Autocomplete, TextField } from "@mui/material";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";

interface CommonProps {
allItems: ItemCombo[];
error?: boolean;
}

interface SingleAutocompleteProps extends CommonProps {
value: number | string | undefined;
onItemSelect: (itemId: number, uom: string, uomId: number) => void | Promise<void>;
// multiple: false;
}

type Props = SingleAutocompleteProps;

const ItemSelect: React.FC<Props> = ({
allItems,
value,
error,
onItemSelect
}) => {
const { t } = useTranslation("item");
const filteredItems = useMemo(() => {
return allItems
}, [allItems])

const options = useMemo(() => {
return [
{
value: -1, // think think sin
label: t("None"),
uom: "",
uomId: -1,
group: "default",
},
...filteredItems.map((i) => ({
value: i.id as number,
label: i.label,
uom: i.uom,
uomId: i.uomId,
group: "existing",
})),
];
}, [t, filteredItems]);

const currentValue = options.find((o) => o.value === value) || options[0];

const onChange = useCallback(
(
event: React.SyntheticEvent,
newValue: { value: number; uom: string; uomId: number; group: string } | { uom: string; uomId: number; value: number }[],
) => {
const singleNewVal = newValue as {
value: number;
uom: string;
uomId: number;
group: string;
};
onItemSelect(singleNewVal.value, singleNewVal.uom, singleNewVal.uomId)
}
, [onItemSelect])
return (
<Autocomplete
noOptionsText={t("No Item")}
disableClearable
fullWidth
value={currentValue}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => <TextField {...params} error={error} />}
/>
);
}
export default ItemSelect

+ 1514
- 0
src/components/Jodetail/JobPickExecution.tsx
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 383
- 0
src/components/Jodetail/JobPickExecutionForm.tsx Прегледај датотеку

@@ -0,0 +1,383 @@
// FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
"use client";

import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
} from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
import { fetchEscalationCombo } from "@/app/api/user/actions";

interface LotPickData {
id: number;
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
stockUnit: string;
inQty: number;
outQty: number;
holdQty: number;
totalPickedByAllPickOrders: number;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
}

interface PickExecutionFormProps {
open: boolean;
onClose: () => void;
onSubmit: (data: PickExecutionIssueData) => Promise<void>;
selectedLot: LotPickData | null;
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// ✅ Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}

// 定义错误类型
interface FormErrors {
actualPickQty?: string;
missQty?: string;
badItemQty?: string;
issueRemark?: string;
handledBy?: string;
}

const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
open,
onClose,
onSubmit,
selectedLot,
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// ✅ Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
const [verifiedQty, setVerifiedQty] = useState<number>(0);
// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
return Math.max(0, remainingQty);
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// ✅ Use the original required quantity, not subtracting actualPickQty
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
try {
const escalationCombo = await fetchEscalationCombo();
setHandlers(escalationCombo);
} catch (error) {
console.error("Error fetching handlers:", error);
}
};
fetchHandlers();
}, []);

// 初始化表单数据 - 每次打开时都重新初始化
useEffect(() => {
if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
const getSafeDate = (dateValue: any): string => {
if (!dateValue) return new Date().toISOString().split('T')[0];
try {
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
return new Date().toISOString().split('T')[0];
}
return date.toISOString().split('T')[0];
} catch {
return new Date().toISOString().split('T')[0];
}
};

// ✅ Initialize verified quantity to the received quantity (actualPickQty)
const initialVerifiedQty = selectedLot.actualPickQty || 0;
setVerifiedQty(initialVerifiedQty);
console.log("=== PickExecutionForm Debug ===");
console.log("selectedLot:", selectedLot);
console.log("initialVerifiedQty:", initialVerifiedQty);
console.log("=== End Debug ===");
setFormData({
pickOrderId: pickOrderId,
pickOrderCode: selectedPickOrderLine.pickOrderCode,
pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
pickExecutionDate: new Date().toISOString().split('T')[0],
pickOrderLineId: selectedPickOrderLine.id,
itemId: selectedPickOrderLine.itemId,
itemCode: selectedPickOrderLine.itemCode,
itemDescription: selectedPickOrderLine.itemName,
lotId: selectedLot.lotId,
lotNo: selectedLot.lotNo,
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: initialVerifiedQty, // ✅ Use the initial value
missQty: 0,
badItemQty: 0,
issueRemark: '',
pickerName: '',
handledBy: undefined,
});
}
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]);

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// ✅ Update verified quantity state when actualPickQty changes
if (field === 'actualPickQty') {
setVerifiedQty(value);
}
// 清除错误
if (errors[field as keyof FormErrors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
}, [errors]);

// ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (verifiedQty === undefined || verifiedQty < 0) {
newErrors.actualPickQty = t('Qty is required');
}
// ✅ Check if verified qty exceeds received qty
if (verifiedQty > (selectedLot?.actualPickQty || 0)) {
newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity');
}
// ✅ Check if verified qty exceeds required qty
if (verifiedQty > (selectedLot?.requiredQty || 0)) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
}
// ✅ Require either missQty > 0 OR badItemQty > 0
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('At least one issue must be reported');
newErrors.badItemQty = t('At least one issue must be reported');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
if (!validateForm() || !formData.pickOrderId) {
return;
}

setLoading(true);
try {
// ✅ Use the verified quantity in the submission
const submissionData = {
...formData,
actualPickQty: verifiedQty
} as PickExecutionIssueData;
await onSubmit(submissionData);
onClose();
} catch (error) {
console.error('Error submitting pick execution issue:', error);
} finally {
setLoading(false);
}
};

const handleClose = () => {
setFormData({});
setErrors({});
setVerifiedQty(0);
onClose();
};

if (!selectedLot || !selectedPickOrderLine) {
return null;
}

const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* ✅ Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
<Typography variant="body2" color="warning.main">
<strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
</Typography>
</Box>
</Grid>
{/* ✅ Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
label={t('Required Qty')}
value={selectedLot?.requiredQty || 0}
disabled
variant="outlined"
// helperText={t('Still need to pick')}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label={t('Received Qty')}
value={formData.actualPickQty || 0}
disabled
variant="outlined"
// helperText={t('Available in warehouse')}
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('Verified Qty')}
type="number"
value={verifiedQty} // ✅ Use the separate state
onChange={(e) => {
const newValue = parseFloat(e.target.value) || 0;
setVerifiedQty(newValue);
handleInputChange('actualPickQty', newValue);
}}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('Missing item Qty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
error={!!errors.missQty}
// helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('Bad Item Qty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
error={!!errors.badItemQty}
// helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
variant="outlined"
/>
</Grid>
{/* ✅ Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>
<TextField
fullWidth
id="issueRemark"
label={t('Issue Remark')}
multiline
rows={4}
value={formData.issueRemark || ''}
onChange={(e) => handleInputChange('issueRemark', e.target.value)}
error={!!errors.issueRemark}
helperText={errors.issueRemark}
//placeholder={t('Describe the issue with bad items')}
variant="outlined"
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth error={!!errors.handledBy}>
<InputLabel>{t('handler')}</InputLabel>
<Select
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
label={t('handler')}
>
{handlers.map((handler) => (
<MenuItem key={handler.id} value={handler.id.toString()}>
{handler.name}
</MenuItem>
))}
</Select>
{errors.handledBy && (
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
{errors.handledBy}
</Typography>
)}
</FormControl>
</Grid>
</>
) : (<></>)}
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('Cancel')}
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={loading}
>
{loading ? t('submitting') : t('submit')}
</Button>
</DialogActions>
</Dialog>
);
};

export default PickExecutionForm;

+ 1295
- 0
src/components/Jodetail/JobPickExecutionsecondscan.tsx
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1824
- 0
src/components/Jodetail/Jobcreatitem.tsx
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 167
- 0
src/components/Jodetail/Jodetail.tsx Прегледај датотеку

@@ -0,0 +1,167 @@
import { Button, CircularProgress, Grid } from "@mui/material";
import SearchResults, { Column } from "../SearchResults/SearchResults";
import { PickOrderResult } from "@/app/api/pickOrder";
import { useTranslation } from "react-i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import { isEmpty, upperCase, upperFirst } from "lodash";
import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
consolidatePickOrder,
fetchPickOrderClient,
} from "@/app/api/pickOrder/actions";
import useUploadContext from "../UploadProvider/useUploadContext";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
dayjs.extend(arraySupport);
interface Props {
filteredPickOrders: PickOrderResult[];
filterArgs: Record<string, any>;
}

const Jodetail: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => {
const { t } = useTranslation("pickOrder");
const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]);
const [filteredPickOrder, setFilteredPickOrder] = useState(
[] as PickOrderResult[],
);
const { setIsUploading } = useUploadContext();
const [isLoading, setIsLoading] = useState(false);
const [pagingController, setPagingController] = useState({
pageNum: 0,
pageSize: 10,
});
const [totalCount, setTotalCount] = useState<number>();

const fetchNewPagePickOrder = useCallback(
async (
pagingController: Record<string, number>,
filterArgs: Record<string, number>,
) => {
setIsLoading(true);
const params = {
...pagingController,
...filterArgs,
};
const res = await fetchPickOrderClient(params);
if (res) {
console.log(res);
setFilteredPickOrder(res.records);
setTotalCount(res.total);
}
setIsLoading(false);
},
[],
);

const handleConsolidatedRows = useCallback(async () => {
console.log(selectedRows);
setIsUploading(true);
try {
const res = await consolidatePickOrder(selectedRows as number[]);
if (res) {
console.log(res);
}
} catch {
setIsUploading(false);
}
fetchNewPagePickOrder(pagingController, filterArgs);
setIsUploading(false);
}, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]);


useEffect(() => {
fetchNewPagePickOrder(pagingController, filterArgs);
}, [fetchNewPagePickOrder, pagingController, filterArgs]);

const columns = useMemo<Column<PickOrderResult>[]>(
() => [
{
name: "id",
label: "",
type: "checkbox",
disabled: (params) => {
return !isEmpty(params.consoCode);
},
},
{
name: "code",
label: t("Code"),
},
{
name: "consoCode",
label: t("Consolidated Code"),
renderCell: (params) => {
return params.consoCode ?? "";
},
},
{
name: "type",
label: t("type"),
renderCell: (params) => {
return upperCase(params.type);
},
},
{
name: "items",
label: t("Items"),
renderCell: (params) => {
return params.items?.map((i) => i.name).join(", ");
},
},
{
name: "targetDate",
label: t("Target Date"),
renderCell: (params) => {
return (
dayjs(params.targetDate)
.add(-1, "month")
.format(OUTPUT_DATE_FORMAT)
);
},
},
{
name: "releasedBy",
label: t("Released By"),
},
{
name: "status",
label: t("Status"),
renderCell: (params) => {
return upperFirst(params.status);
},
},
],
[t],
);

return (
<Grid container rowGap={1}>
<Grid item xs={3}>
<Button
disabled={selectedRows.length < 1}
variant="outlined"
onClick={handleConsolidatedRows}
>
{t("Consolidate")}
</Button>
</Grid>
<Grid item xs={12}>
{isLoading ? (
<CircularProgress size={40} />
) : (
<SearchResults<PickOrderResult>
items={filteredPickOrder}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={totalCount}
checkboxIds={selectedRows!}
setCheckboxIds={setSelectedRows}
/>
)}
</Grid>
</Grid>
);
};

export default Jodetail;

+ 443
- 0
src/components/Jodetail/JodetailSearch.tsx Прегледај датотеку

@@ -0,0 +1,443 @@
"use client";
import { PickOrderResult } from "@/app/api/pickOrder";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SearchBox, { Criterion } from "../SearchBox";
import {
flatten,
intersectionWith,
isEmpty,
sortBy,
uniqBy,
upperCase,
upperFirst,
} from "lodash";
import {
arrayToDayjs,
} from "@/app/utils/formatUtil";
import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material";
import Jodetail from "./Jodetail"
import PickExecution from "./JobPickExecution";
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore } from "@/app/api/pickOrder/actions";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan";
import FInishedJobOrderRecord from "./FInishedJobOrderRecord";
import JobPickExecution from "./JobPickExecution";
import {
fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder,
fetchJobOrderLotsHierarchical,
fetchCompletedJobOrderPickOrders,
fetchCompletedJobOrderPickOrderRecords
} from "@/app/api/jo/actions";
interface Props {
pickOrders: PickOrderResult[];
}

type SearchQuery = Partial<
Omit<PickOrderResult, "id" | "consoCode" | "completeDate">
>;

type SearchParamNames = keyof SearchQuery;

const JodetailSearch: React.FC<Props> = ({ pickOrders }) => {
const { t } = useTranslation("jo");
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;

const [isOpenCreateModal, setIsOpenCreateModal] = useState(false)
const [items, setItems] = useState<ItemCombo[]>([])
const [printButtonsEnabled, setPrintButtonsEnabled] = useState(false);
const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders);
const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [tabIndex, setTabIndex] = useState(0);
const [totalCount, setTotalCount] = useState<number>();
const [isAssigning, setIsAssigning] = useState(false);
const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>(
typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
);
useEffect(() => {
const onAssigned = () => {
localStorage.removeItem('hideCompletedUntilNext');
setHideCompletedUntilNext(false);
};
window.addEventListener('pickOrderAssigned', onAssigned);
return () => window.removeEventListener('pickOrderAssigned', onAssigned);
}, []);
// ... existing code ...

useEffect(() => {
const handleCompletionStatusChange = (event: CustomEvent) => {
const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail;
// ✅ 修复:根据标签页和事件来源决定是否更新打印按钮状态
if (eventTabIndex === undefined || eventTabIndex === tabIndex) {
setPrintButtonsEnabled(allLotsCompleted);
console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted);
}
};

window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
return () => {
window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
};
}, [tabIndex]); // ✅ 添加 tabIndex 依赖

// ✅ 新增:处理标签页切换时的打印按钮状态重置
useEffect(() => {
// 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态
if (tabIndex === 2) {
setPrintButtonsEnabled(false);
console.log("Reset print buttons for Pick Execution Record tab");
}
}, [tabIndex]);

// ... existing code ...
const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
setIsAssigning(true);
try {
const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
console.log("Assign by store result:", res);
// ✅ Handle different response codes
if (res.code === "SUCCESS") {
console.log("✅ Successfully assigned pick order to store", storeId);
// ✅ Trigger refresh to show newly assigned data
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "USER_BUSY") {
console.warn("⚠️ User already has pick orders in progress:", res.message);
// ✅ Show warning but still refresh to show existing orders
alert(`Warning: ${res.message}`);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "NO_ORDERS") {
console.log("ℹ️ No available pick orders for store", storeId);
alert(`Info: ${res.message}`);
} else {
console.log("ℹ️ Assignment result:", res.message);
alert(`Info: ${res.message}`);
}
} catch (error) {
console.error("❌ Error assigning by store:", error);
alert("Error occurred during assignment");
} finally {
setIsAssigning(false);
}
};
// ✅ Manual assignment handler - uses the action function
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
try {
const orders = await fetchUnassignedJobOrderPickOrders();
setUnassignedOrders(orders);
} catch (error) {
console.error("Error loading unassigned orders:", error);
} finally {
setIsLoadingUnassigned(false);
}
}, []);
// 分配订单给当前用户
const handleAssignOrder = useCallback(async (pickOrderId: number) => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
try {
const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
if (result.message === "Successfully assigned") {
console.log("✅ Successfully assigned pick order");
// 刷新数据
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
// 重新加载未分配订单列表
loadUnassignedOrders();
} else {
console.warn("⚠️ Assignment failed:", result.message);
alert(`Assignment failed: ${result.message}`);
}
} catch (error) {
console.error("❌ Error assigning order:", error);
alert("Error occurred during assignment");
}
}, [currentUserId, loadUnassignedOrders]);
// 在组件加载时获取未分配订单
useEffect(() => {
loadUnassignedOrders();
}, [loadUnassignedOrders]);

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);
const openCreateModal = useCallback(async () => {
console.log("testing")
const res = await fetchAllItemsInClient()
console.log(res)
setItems(res)
setIsOpenCreateModal(true)
}, [])

const closeCreateModal = useCallback(() => {
setIsOpenCreateModal(false)
}, [])

useEffect(() => {
if (tabIndex === 3) {
const loadItems = async () => {
try {
const itemsData = await fetchAllItemsInClient();
console.log("PickOrderSearch loaded items:", itemsData.length);
setItems(itemsData);
} catch (error) {
console.error("Error loading items in PickOrderSearch:", error);
}
};
// 如果还没有数据,则加载
if (items.length === 0) {
loadItems();
}
}
}, [tabIndex, items.length]);
useEffect(() => {
const handleCompletionStatusChange = (event: CustomEvent) => {
const { allLotsCompleted } = event.detail;
setPrintButtonsEnabled(allLotsCompleted);
console.log("Print buttons enabled:", allLotsCompleted);
};

window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
return () => {
window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
};
}, []);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => {
const baseCriteria: Criterion<SearchParamNames>[] = [
{
label: tabIndex === 3 ? t("Item Code") : t("Code"),
paramName: "code",
type: "text"
},
{
label: t("Type"),
paramName: "type",
type: "autocomplete",
options: tabIndex === 3
?
[
{ value: "Consumable", label: t("Consumable") },
{ value: "Material", label: t("Material") },
{ value: "Product", label: t("Product") }
]
:
sortBy(
uniqBy(
pickOrders.map((po) => ({
value: po.type,
label: t(upperCase(po.type)),
})),
"value",
),
"label",
),
},
];
// Add Job Order search for Create Item tab (tabIndex === 3)
if (tabIndex === 3) {
baseCriteria.splice(1, 0, {
label: t("Job Order"),
paramName: "jobOrderCode" as any, // Type assertion for now
type: "text",
});
baseCriteria.splice(2, 0, {
label: t("Target Date"),
paramName: "targetDate",
type: "date",
});
} else {
baseCriteria.splice(1, 0, {
label: t("Target Date From"),
label2: t("Target Date To"),
paramName: "targetDate",
type: "dateRange",
});
}
// Add Items/Item Name criteria
baseCriteria.push({
label: tabIndex === 3 ? t("Item Name") : t("Items"),
paramName: "items",
type: tabIndex === 3 ? "text" : "autocomplete",
options: tabIndex === 3
? []
:
uniqBy(
flatten(
sortBy(
pickOrders.map((po) =>
po.items
? po.items.map((item) => ({
value: item.name,
label: item.name,
}))
: [],
),
"label",
),
),
"value",
),
});
// Add Status criteria for non-Create Item tabs
if (tabIndex !== 3) {
baseCriteria.push({
label: t("Status"),
paramName: "status",
type: "autocomplete",
options: sortBy(
uniqBy(
pickOrders.map((po) => ({
value: po.status,
label: t(upperFirst(po.status)),
})),
"value",
),
"label",
),
});
}
return baseCriteria;
},
[pickOrders, t, tabIndex, items],
);

const fetchNewPagePickOrder = useCallback(
async (
pagingController: Record<string, number>,
filterArgs: Record<string, number>,
) => {
const params = {
...pagingController,
...filterArgs,
};
const res = await fetchPickOrderClient(params);
if (res) {
console.log(res);
setFilteredPickOrders(res.records);
setTotalCount(res.total);
}
},
[],
);

const onReset = useCallback(() => {
setFilteredPickOrders(pickOrders);
}, [pickOrders]);

useEffect(() => {
if (!isOpenCreateModal) {
setTabIndex(1)
setTimeout(async () => {
setTabIndex(0)
}, 200)
}
}, [isOpenCreateModal])
// 添加处理提料单创建成功的函数
const handlePickOrderCreated = useCallback(() => {
// 切换到 Assign & Release 标签页 (tabIndex = 1)
setTabIndex(2);
}, []);

return (
<Box sx={{
height: '100vh', // Full viewport height
overflow: 'auto' // Single scrollbar for the whole page
}}>
{/* Header section */}
<Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}>
<Stack rowGap={2}>
<Grid container alignItems="center">
<Grid item xs={8}>

</Grid>

{/* Last 2 buttons aligned right */}
<Grid item xs={6} >
{/* Unassigned Job Orders */}
{unassignedOrders.length > 0 && (
<Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
{t("Unassigned Job Orders")} ({unassignedOrders.length})
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap">
{unassignedOrders.map((order) => (
<Button
key={order.pickOrderId}
variant="outlined"
size="small"
onClick={() => handleAssignOrder(order.pickOrderId)}
disabled={isLoadingUnassigned}
>
{order.pickOrderCode} - {order.jobOrderName}
</Button>
))}
</Stack>
</Box>
)}
</Grid>


</Grid>
</Stack>
</Box>

{/* Tabs section - ✅ Move the click handler here */}
<Box sx={{
borderBottom: '1px solid #e0e0e0'
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Pick Order Detail")} iconPosition="end" />
<Tab label={t("Job Order Match")} iconPosition="end" />
<Tab label={t("Finished Job Order Record")} iconPosition="end" />
</Tabs>
</Box>

{/* Content section - NO overflow: 'auto' here */}
<Box sx={{
p: 2
}}>
{tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />}
{tabIndex === 1 && <JobPickExecutionsecondscan filterArgs={filterArgs} />}
{tabIndex === 2 && <FInishedJobOrderRecord filterArgs={filterArgs} />}
</Box>
</Box>
);
};

export default JodetailSearch;

+ 124
- 0
src/components/Jodetail/LotConfirmationModal.tsx Прегледај датотеку

@@ -0,0 +1,124 @@
"use client";

import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
Alert,
Stack,
Divider,
} from "@mui/material";
import { useTranslation } from "react-i18next";

interface LotConfirmationModalProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
expectedLot: {
lotNo: string;
itemCode: string;
itemName: string;
};
scannedLot: {
lotNo: string;
itemCode: string;
itemName: string;
};
isLoading?: boolean;
}

const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({
open,
onClose,
onConfirm,
expectedLot,
scannedLot,
isLoading = false,
}) => {
const { t } = useTranslation("pickOrder");

return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>
<Typography variant="h6" component="div" color="warning.main">
{t("Lot Number Mismatch")}
</Typography>
</DialogTitle>
<DialogContent>
<Stack spacing={3}>
<Alert severity="warning">
{t("The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?")}
</Alert>

<Box>
<Typography variant="subtitle1" gutterBottom color="primary">
{t("Expected Lot:")}
</Typography>
<Box sx={{ pl: 2, py: 1, backgroundColor: 'grey.50', borderRadius: 1 }}>
<Typography variant="body2">
<strong>{t("Item Code")}:</strong> {expectedLot.itemCode}
</Typography>
<Typography variant="body2">
<strong>{t("Item Name")}:</strong> {expectedLot.itemName}
</Typography>
<Typography variant="body2">
<strong>{t("Lot No")}:</strong> {expectedLot.lotNo}
</Typography>
</Box>
</Box>

<Divider />

<Box>
<Typography variant="subtitle1" gutterBottom color="warning.main">
{t("Scanned Lot:")}
</Typography>
<Box sx={{ pl: 2, py: 1, backgroundColor: 'warning.50', borderRadius: 1 }}>
<Typography variant="body2">
<strong>{t("Item Code")}:</strong> {scannedLot.itemCode}
</Typography>
<Typography variant="body2">
<strong>{t("Item Name")}:</strong> {scannedLot.itemName}
</Typography>
<Typography variant="body2">
<strong>{t("Lot No")}:</strong> {scannedLot.lotNo}
</Typography>
</Box>
</Box>

<Alert severity="info">
{t("If you confirm, the system will:")}
<ul style={{ margin: '8px 0 0 16px' }}>
<li>{t("Update your suggested lot to the this scanned lot")}</li>
</ul>
</Alert>
</Stack>
</DialogContent>

<DialogActions>
<Button
onClick={onClose}
variant="outlined"
disabled={isLoading}
>
{t("Cancel")}
</Button>
<Button
onClick={onConfirm}
variant="contained"
color="warning"
disabled={isLoading}
>
{isLoading ? t("Processing...") : t("Confirm")}
</Button>
</DialogActions>
</Dialog>
);
};

export default LotConfirmationModal;

+ 527
- 0
src/components/Jodetail/PutawayForm.tsx Прегледај датотеку

@@ -0,0 +1,527 @@
"use client";

import { PurchaseQcResult, PutAwayInput, PutAwayLine } from "@/app/api/po/actions";
import {
Autocomplete,
Box,
Button,
Card,
CardContent,
FormControl,
Grid,
Modal,
ModalProps,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { QcItemWithChecks } from "@/app/api/qc";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { WarehouseResult } from "@/app/api/warehouse";
import {
OUTPUT_DATE_FORMAT,
stockInLineStatusMap,
} from "@/app/utils/formatUtil";
import { QRCodeSVG } from "qrcode.react";
import { QrCode } from "../QrCode";
import ReactQrCodeScanner, {
ScannerConfig,
} from "../ReactQrCodeScanner/ReactQrCodeScanner";
import { QrCodeInfo } from "@/app/api/qrcode";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import { dummyPutawayLine } from "./dummyQcTemplate";
dayjs.extend(arraySupport);

interface Props {
itemDetail: StockInLine;
warehouse: WarehouseResult[];
disabled: boolean;
// qc: QcItemWithChecks[];
}
type EntryError =
| {
[field in keyof PutAwayLine]?: string;
}
| undefined;

type PutawayRow = TableRow<Partial<PutAwayLine>, EntryError>;

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: 5,
px: 5,
pb: 10,
width: "auto",
};

const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled }) => {
const { t } = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<PutAwayInput>();
console.log(itemDetail);
// const [recordQty, setRecordQty] = useState(0);
const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId);
const filteredWarehouse = useMemo(() => {
// do filtering here if any
return warehouse;
}, []);

const defaultOption = {
value: 0, // think think sin
label: t("Select warehouse"),
group: "default",
};
const options = useMemo(() => {
return [
// {
// value: 0, // think think sin
// label: t("Select warehouse"),
// group: "default",
// },
...filteredWarehouse.map((w) => ({
value: w.id,
label: `${w.code} - ${w.name}`,
group: "existing",
})),
];
}, [filteredWarehouse]);
const currentValue =
warehouseId > 0
? options.find((o) => o.value === warehouseId)
: options.find((o) => o.value === getValues("warehouseId")) ||
defaultOption;

const onChange = useCallback(
(
event: React.SyntheticEvent,
newValue: { value: number; group: string } | { value: number }[],
) => {
const singleNewVal = newValue as {
value: number;
group: string;
};
console.log(singleNewVal);
console.log("onChange");
// setValue("warehouseId", singleNewVal.value);
setWarehouseId(singleNewVal.value);
},
[],
);
console.log(watch("putAwayLines"))
// const accQty = watch("acceptedQty");
// const validateForm = useCallback(() => {
// console.log(accQty);
// if (accQty > itemDetail.acceptedQty) {
// setError("acceptedQty", {
// message: `acceptedQty must not greater than ${itemDetail.acceptedQty}`,
// type: "required",
// });
// }
// if (accQty < 1) {
// setError("acceptedQty", {
// message: `minimal value is 1`,
// type: "required",
// });
// }
// if (isNaN(accQty)) {
// setError("acceptedQty", {
// message: `value must be a number`,
// type: "required",
// });
// }
// }, [accQty]);

// useEffect(() => {
// clearErrors();
// validateForm();
// }, [validateForm]);

const qrContent = useMemo(
() => ({
stockInLineId: itemDetail.id,
itemId: itemDetail.itemId,
lotNo: itemDetail.lotNo,
// warehouseId: 2 // for testing
// expiryDate: itemDetail.expiryDate,
// productionDate: itemDetail.productionDate,
// supplier: itemDetail.supplier,
// poCode: itemDetail.poCode,
}),
[itemDetail],
);
const [isOpenScanner, setOpenScanner] = useState(false);

const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
setOpenScanner(false);
},
[],
);

const onOpenScanner = useCallback(() => {
setOpenScanner(true);
}, []);

const onCloseScanner = useCallback(() => {
setOpenScanner(false);
}, []);
const scannerConfig = useMemo<ScannerConfig>(
() => ({
onUpdate: (err, result) => {
console.log(result);
console.log(Boolean(result));
if (result) {
const data: QrCodeInfo = JSON.parse(result.getText());
console.log(data);
if (data.warehouseId) {
console.log(data.warehouseId);
setWarehouseId(data.warehouseId);
onCloseScanner();
}
} else return;
},
}),
[onCloseScanner],
);

// QR Code Scanner
const scanner = useQrCodeScannerContext();
useEffect(() => {
if (isOpenScanner) {
scanner.startScan();
} else if (!isOpenScanner) {
scanner.stopScan();
}
}, [isOpenScanner]);

useEffect(() => {
if (scanner.values.length > 0) {
console.log(scanner.values[0]);
const data: QrCodeInfo = JSON.parse(scanner.values[0]);
console.log(data);
if (data.warehouseId) {
console.log(data.warehouseId);
setWarehouseId(data.warehouseId);
onCloseScanner();
}
scanner.resetScan();
}
}, [scanner.values]);

useEffect(() => {
setValue("status", "completed");
setValue("warehouseId", options[0].value);
}, []);

useEffect(() => {
if (warehouseId > 0) {
setValue("warehouseId", warehouseId);
clearErrors("warehouseId");
}
}, [warehouseId]);

const getWarningTextHardcode = useCallback((): string | undefined => {
console.log(options)
if (options.length === 0) return undefined
const defaultWarehouseId = options[0].value;
const currWarehouseId = watch("warehouseId");
if (defaultWarehouseId !== currWarehouseId) {
return t("not default warehosue");
}
return undefined;
}, [options]);

const columns = useMemo<GridColDef[]>(
() => [
{
field: "qty",
headerName: t("qty"),
flex: 1,
// renderCell(params) {
// return <>100</>
// },
},
{
field: "warehouse",
headerName: t("warehouse"),
flex: 1,
// renderCell(params) {
// return <>{filteredWarehouse[0].name}</>
// },
},
{
field: "printQty",
headerName: t("printQty"),
flex: 1,
// renderCell(params) {
// return <>100</>
// },
},
], [])

const validation = useCallback(
(newRow: GridRowModel<PutawayRow>): EntryError => {
const error: EntryError = {};
const { qty, warehouseId, printQty } = newRow;

return Object.keys(error).length > 0 ? error : undefined;
},
[],
);

return (
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Putaway Detail")}
</Typography>
</Grid>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={12}>
<TextField
label={t("LotNo")}
fullWidth
value={itemDetail.lotNo}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Supplier")}
fullWidth
value={itemDetail.supplier}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Po Code")}
fullWidth
value={itemDetail.poCode}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemName")}
fullWidth
value={itemDetail.itemName}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemNo")}
fullWidth
value={itemDetail.itemNo}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("qty")}
fullWidth
value={itemDetail.acceptedQty}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("productionDate")}
fullWidth
value={
// dayjs(itemDetail.productionDate)
dayjs()
// .add(-1, "month")
.format(OUTPUT_DATE_FORMAT)}
disabled
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("expiryDate")}
fullWidth
value={
// dayjs(itemDetail.expiryDate)
dayjs()
.add(20, "day")
.format(OUTPUT_DATE_FORMAT)}
disabled
/>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<Autocomplete
noOptionsText={t("No Warehouse")}
disableClearable
disabled
fullWidth
defaultValue={options[0]} /// modify this later
// onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => (
<TextField {...params} label={t("Default Warehouse")} />
)}
/>
</FormControl>
</Grid>
{/* <Grid item xs={5.5}>
<TextField
label={t("acceptedQty")}
fullWidth
{...register("acceptedQty", {
required: "acceptedQty required!",
min: 1,
max: itemDetail.acceptedQty,
valueAsNumber: true,
})}
// defaultValue={itemDetail.acceptedQty}
disabled={disabled}
error={Boolean(errors.acceptedQty)}
helperText={errors.acceptedQty?.message}
/>
</Grid>
<Grid item xs={1}>
<Button disabled={disabled} onClick={onOpenScanner}>
{t("bind")}
</Button>
</Grid> */}
{/* <Grid item xs={5.5}>
<Controller
control={control}
name="warehouseId"
render={({ field }) => {
console.log(field);
return (
<Autocomplete
noOptionsText={t("No Warehouse")}
disableClearable
fullWidth
value={options.find((o) => o.value == field.value)}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => (
<TextField
{...params}
label={"Select warehouse"}
error={Boolean(errors.warehouseId?.message)}
helperText={warehouseHelperText}
// helperText={errors.warehouseId?.message}
/>
)}
/>
);
}}
/>
<FormControl fullWidth>
<Autocomplete
noOptionsText={t("No Warehouse")}
disableClearable
fullWidth
// value={warehouseId > 0
// ? options.find((o) => o.value === warehouseId)
// : undefined}
defaultValue={options[0]}
// defaultValue={options.find((o) => o.value === 1)}
value={currentValue}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => (
<TextField
{...params}
// label={"Select warehouse"}
disabled={disabled}
error={Boolean(errors.warehouseId?.message)}
helperText={
errors.warehouseId?.message ?? getWarningTextHardcode()
}
// helperText={warehouseHelperText}
/>
)}
/>
</FormControl>
</Grid> */}
<Grid
item
xs={12}
style={{ display: "flex", justifyContent: "center" }}
>
{/* <QrCode content={qrContent} sx={{ width: 200, height: 200 }} /> */}
<InputDataGrid<PutAwayInput, PutAwayLine, EntryError>
apiRef={apiRef}
checkboxSelection={false}
_formKey={"putAwayLines"}
columns={columns}
validateRow={validation}
needAdd={true}
showRemoveBtn={false}
/>
</Grid>
</Grid>
{/* <Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Button onClick={onOpenScanner}>bind</Button>
</Grid> */}

<Modal open={isOpenScanner} onClose={closeHandler}>
<Box sx={style}>
<Typography variant="h4">
{t("Please scan warehouse qr code.")}
</Typography>
{/* <ReactQrCodeScanner scannerConfig={scannerConfig} /> */}
</Box>
</Modal>
</Grid>
);
};
export default PutawayForm;

+ 395
- 0
src/components/Jodetail/QCDatagrid.tsx Прегледај датотеку

@@ -0,0 +1,395 @@
"use client";
import {
Dispatch,
MutableRefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import StyledDataGrid from "../StyledDataGrid";
import {
FooterPropsOverrides,
GridActionsCellItem,
GridCellParams,
GridColDef,
GridEventListener,
GridRowEditStopReasons,
GridRowId,
GridRowIdGetter,
GridRowModel,
GridRowModes,
GridRowModesModel,
GridRowSelectionModel,
GridToolbarContainer,
GridValidRowModel,
useGridApiRef,
} from "@mui/x-data-grid";
import { set, useFormContext } from "react-hook-form";
import SaveIcon from "@mui/icons-material/Save";
import DeleteIcon from "@mui/icons-material/Delete";
import CancelIcon from "@mui/icons-material/Cancel";
import { Add } from "@mui/icons-material";
import { Box, Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import {
GridApiCommunity,
GridSlotsComponentsProps,
} from "@mui/x-data-grid/internals";
import { dummyQCData } from "./dummyQcTemplate";
// T == CreatexxxInputs map of the form's fields
// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc
// E == error
interface ResultWithId {
id: string | number;
}
// export type InputGridProps = {
// [key: string]: any
// }
interface DefaultResult<E> {
_isNew: boolean;
_error: E;
}

interface SelectionResult<E> {
active: boolean;
_isNew: boolean;
_error: E;
}
type Result<E> = DefaultResult<E> | SelectionResult<E>;

export type TableRow<V, E> = Partial<
V & {
isActive: boolean | undefined;
_isNew: boolean;
_error: E;
} & ResultWithId
>;

export interface InputDataGridProps<T, V, E> {
apiRef: MutableRefObject<GridApiCommunity>;
// checkboxSelection: false | undefined;
_formKey: keyof T;
columns: GridColDef[];
validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E;
needAdd?: boolean;
}

export interface SelectionInputDataGridProps<T, V, E> {
// thinking how do
apiRef: MutableRefObject<GridApiCommunity>;
// checkboxSelection: true;
_formKey: keyof T;
columns: GridColDef[];
validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E;
}

export type Props<T, V, E> =
| InputDataGridProps<T, V, E>
| SelectionInputDataGridProps<T, V, E>;
export class ProcessRowUpdateError<T, E> extends Error {
public readonly row: T;
public readonly errors: E | undefined;
constructor(row: T, message?: string, errors?: E) {
super(message);
this.row = row;
this.errors = errors;

Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
}
}
// T == CreatexxxInputs map of the form's fields
// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc
// E == error
function InputDataGrid<T, V, E>({
apiRef,
// checkboxSelection = false,
_formKey,
columns,
validateRow,
}: Props<T, V, E>) {
const {
t,
// i18n: { language },
} = useTranslation("purchaseOrder");
const formKey = _formKey.toString();
const { setValue, getValues } = useFormContext();
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
// const apiRef = useGridApiRef();
const getRowId = useCallback<GridRowIdGetter<TableRow<V, E>>>(
(row) => row.id! as number,
[],
);
const formValue = getValues(formKey)
const list: TableRow<V, E>[] = !formValue || formValue.length == 0 ? dummyQCData : getValues(formKey);
console.log(list)
const [rows, setRows] = useState<TableRow<V, E>[]>(() => {
// const list: TableRow<V, E>[] = getValues(formKey);
console.log(list)
return list && list.length > 0 ? list : [];
});
console.log(rows)
// const originalRows = list && list.length > 0 ? list : [];
const originalRows = useMemo(() => (
list && list.length > 0 ? list : []
), [list])
// const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel
const [rowSelectionModel, setRowSelectionModel] =
useState<GridRowSelectionModel>(() => {
// const rowModel = list.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel
const rowModel: GridRowSelectionModel = getValues(
`${formKey}_active`,
) as GridRowSelectionModel;
console.log(rowModel);
return rowModel;
});

useEffect(() => {
for (let i = 0; i < rows.length; i++) {
const currRow = rows[i]
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[currRow.id as number]: { mode: GridRowModes.View },
}));
}
}, [rows])

const handleSave = useCallback(
(id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View },
}));
},
[],
);
const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError<T, E>) => {
const errors = updateError.errors;
const row = updateError.row;
console.log(errors);
apiRef.current.updateRows([{ ...row, _error: errors }]);
},
[apiRef],
);

const processRowUpdate = useCallback(
(
newRow: GridRowModel<TableRow<V, E>>,
originalRow: GridRowModel<TableRow<V, E>>,
) => {
/////////////////
// validation here
const errors = validateRow(newRow);
console.log(newRow);
if (errors) {
throw new ProcessRowUpdateError(
originalRow,
"validation error",
errors,
);
}
/////////////////
const { _isNew, _error, ...updatedRow } = newRow;
const rowToSave = {
...updatedRow,
} as TableRow<V, E>; /// test
console.log(rowToSave);
setRows((rw) =>
rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)),
);
return rowToSave;
},
[validateRow, getRowId],
);

const addRow = useCallback(() => {
const newEntry = { id: Date.now(), _isNew: true } as TableRow<V, E>;
setRows((prev) => [...prev, newEntry]);
setRowModesModel((model) => ({
...model,
[getRowId(newEntry)]: {
mode: GridRowModes.Edit,
// fieldToFocus: "team", /// test
},
}));
}, [getRowId]);

const reset = useCallback(() => {
setRowModesModel({});
setRows(originalRows);
}, [originalRows]);

const handleCancel = useCallback(
(id: GridRowId) => () => {
setRowModesModel((model) => ({
...model,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
}));
const editedRow = rows.find((row) => getRowId(row) === id);
if (editedRow?._isNew) {
setRows((rw) => rw.filter((r) => getRowId(r) !== id));
} else {
setRows((rw) =>
rw.map((r) => (getRowId(r) === id ? { ...r, _error: undefined } : r)),
);
}
},
[rows, getRowId],
);

const handleDelete = useCallback(
(id: GridRowId) => () => {
setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id));
},
[getRowId],
);

const _columns = useMemo<GridColDef[]>(
() => [
...columns,
{
field: "actions",
type: "actions",
headerName: "",
flex: 0.5,
cellClassName: "actions",
getActions: ({ id }: { id: GridRowId }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
key="edit"
sx={{
color: "primary.main",
}}
onClick={handleSave(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancel(id)}
/>,
];
}
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: "error.main",
}}
onClick={handleDelete(id)}
color="inherit"
key="edit"
/>,
];
},
},
],
[columns, rowModesModel, handleSave, handleCancel, handleDelete],
);
// sync useForm
useEffect(() => {
// console.log(formKey)
// console.log(rows)
setValue(formKey, rows);
}, [formKey, rows, setValue]);

const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
新增
{/* {t("Add Record")} */}
</Button>
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={reset}
size="small"
>
{/* {t("Clean Record")} */}
清除
</Button>
</Box>
);
// const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
// if (params.reason === GridRowEditStopReasons.rowFocusOut) {
// event.defaultMuiPrevented = true;
// }
// };

return (
<StyledDataGrid
// {...props}
// getRowId={getRowId as GridRowIdGetter<GridValidRowModel>}
rowSelectionModel={rowSelectionModel}
apiRef={apiRef}
rows={rows}
columns={columns}
editMode="row"
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
processRowUpdate={processRowUpdate as any}
// onRowEditStop={handleRowEditStop}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<TableRow<T, E>>) => {
let classname = "";
if (params.row._error) {
classname = "hasError";
}
return classname;
}}
slots={{
// footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
// slotProps={{
// footer: { child: footer },
// }
// }
/>
);
}
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};
const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some entries!")}</Typography>
</Box>
);
};
export default InputDataGrid;

+ 460
- 0
src/components/Jodetail/QcFormVer2.tsx Прегледај датотеку

@@ -0,0 +1,460 @@
"use client";

import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions";
import {
Box,
Card,
CardContent,
Checkbox,
FormControl,
FormControlLabel,
Grid,
Radio,
RadioGroup,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { useFormContext, Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
GridRowSelectionModel,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { stockInLineStatusMap } from "@/app/utils/formatUtil";
import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import axios from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import axiosInstance from "@/app/(main)/axios/axiosInstance";
import EscalationComponent from "./EscalationComponent";
import QcDataGrid from "./QCDatagrid";
import StockInFormVer2 from "./StockInFormVer2";
import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate";
import { ModalFormInput } from "@/app/api/po/actions";
import { escape } from "lodash";

interface Props {
itemDetail: StockInLine;
qc: QcItemWithChecks[];
disabled: boolean;
qcItems: QcData[]
setQcItems: Dispatch<SetStateAction<QcData[]>>
}

type EntryError =
| {
[field in keyof QcData]?: string;
}
| undefined;

type QcRow = TableRow<Partial<QcData>, EntryError>;
// fetchQcItemCheck
const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => {
const { t } = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<PurchaseQCInput>();
const [tabIndex, setTabIndex] = useState(0);
const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>();
const [escalationHistory, setEscalationHistory] = useState(dummyEscalationHistory);
const [qcResult, setQcResult] = useState();
const qcAccept = watch("qcAccept");
// const [qcAccept, setQcAccept] = useState(true);
// const [qcItems, setQcItems] = useState(dummyQCData)

const column = useMemo<GridColDef[]>(
() => [
{
field: "escalation",
headerName: t("escalation"),
flex: 1,
},
{
field: "supervisor",
headerName: t("supervisor"),
flex: 1,
},
], []
)
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

//// validate form
const accQty = watch("acceptQty");
const validateForm = useCallback(() => {
console.log(accQty);
if (accQty > itemDetail.acceptedQty) {
setError("acceptQty", {
message: `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty
}`,
type: "required",
});
}
if (accQty < 1) {
setError("acceptQty", {
message: t("minimal value is 1"),
type: "required",
});
}
if (isNaN(accQty)) {
setError("acceptQty", {
message: t("value must be a number"),
type: "required",
});
}
}, [accQty]);

useEffect(() => {
clearErrors();
validateForm();
}, [clearErrors, validateForm]);

const columns = useMemo<GridColDef[]>(
() => [
{
field: "escalation",
headerName: t("escalation"),
flex: 1,
},
{
field: "supervisor",
headerName: t("supervisor"),
flex: 1,
},
],
[],
);
/// validate datagrid
const validation = useCallback(
(newRow: GridRowModel<QcRow>): EntryError => {
const error: EntryError = {};
// const { qcItemId, failQty } = newRow;
return Object.keys(error).length > 0 ? error : undefined;
},
[],
);

function BooleanEditCell(params: GridRenderEditCellParams) {
const apiRef = useGridApiContext();
const { id, field, value } = params;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
apiRef.current.setEditCellValue({ id, field, value: e.target.checked });
apiRef.current.stopCellEditMode({ id, field }); // commit immediately
};

return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />;
}

const qcColumns: GridColDef[] = [
{
field: "qcItem",
headerName: t("qcItem"),
flex: 2,
renderCell: (params) => (
<Box>
<b>{params.value}</b><br/>
{params.row.qcDescription}<br/>
</Box>
),
},
{
field: 'isPassed',
headerName: t("qcResult"),
flex: 1.5,
renderCell: (params) => {
const currentValue = params.value;
return (
<FormControl>
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
value={currentValue === undefined ? "" : (currentValue ? "true" : "false")}
onChange={(e) => {
const value = e.target.value;
setQcItems((prev) =>
prev.map((r): QcData => (r.id === params.id ? { ...r, isPassed: value === "true" } : r))
);
}}
name={`isPassed-${params.id}`}
>
<FormControlLabel
value="true"
control={<Radio />}
label="合格"
sx={{
color: currentValue === true ? "green" : "inherit",
"& .Mui-checked": {color: "green"}
}}
/>
<FormControlLabel
value="false"
control={<Radio />}
label="不合格"
sx={{
color: currentValue === false ? "red" : "inherit",
"& .Mui-checked": {color: "red"}
}}
/>
</RadioGroup>
</FormControl>
);
},
},
{
field: "failedQty",
headerName: t("failedQty"),
flex: 1,
// editable: true,
renderCell: (params) => (
<TextField
type="number"
size="small"
value={!params.row.isPassed? (params.value ?? '') : '0'}
disabled={params.row.isPassed}
onChange={(e) => {
const v = e.target.value;
const next = v === '' ? undefined : Number(v);
if (Number.isNaN(next)) return;
setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, failedQty: next } : r))
);
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
inputProps={{ min: 0 }}
sx={{ width: '100%' }}
/>
),
},
{
field: "remarks",
headerName: t("remarks"),
flex: 2,
renderCell: (params) => (
<TextField
size="small"
value={params.value ?? ''}
onChange={(e) => {
const remarks = e.target.value;
// const next = v === '' ? undefined : Number(v);
// if (Number.isNaN(next)) return;
setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r))
);
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
inputProps={{ min: 0 }}
sx={{ width: '100%' }}
/>
),
},
]

useEffect(() => {
console.log(itemDetail);
}, [itemDetail]);

// Set initial value for acceptQty
useEffect(() => {
if (itemDetail?.acceptedQty !== undefined) {
setValue("acceptQty", itemDetail.acceptedQty);
}
}, [itemDetail?.acceptedQty, setValue]);

// const [openCollapse, setOpenCollapse] = useState(false)
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);

const onFailedOpenCollapse = useCallback((qcItems: QcData[]) => {
const isFailed = qcItems.some((qc) => !qc.isPassed)
console.log(isFailed)
if (isFailed) {
setIsCollapsed(true)
} else {
setIsCollapsed(false)
}
}, [])

// const handleRadioChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value === 'true';
// setValue("qcAccept", value);
// }, [setValue]);


useEffect(() => {
console.log(itemDetail);
}, [itemDetail]);

useEffect(() => {
// onFailedOpenCollapse(qcItems) // This function is no longer needed
}, [qcItems]); // Removed onFailedOpenCollapse from dependency array

return (
<>
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={12}>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab label={t("QC Info")} iconPosition="end" />
<Tab label={t("Escalation History")} iconPosition="end" />
</Tabs>
</Grid>
{tabIndex == 0 && (
<>
<Grid item xs={12}>
{/* <QcDataGrid<ModalFormInput, QcData, EntryError>
apiRef={apiRef}
columns={qcColumns}
_formKey="qcResult"
validateRow={validation}
/> */}
<StyledDataGrid
columns={qcColumns}
rows={qcItems}
autoHeight
/>
</Grid>
{/* <Grid item xs={12}>
<EscalationComponent
forSupervisor={false}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</Grid> */}
</>
)}
{tabIndex == 1 && (
<>
{/* <Grid item xs={12}>
<StockInFormVer2
itemDetail={itemDetail}
disabled={false}
/>
</Grid> */}
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Escalation Info")}
</Typography>
</Grid>
<Grid item xs={12}>
<StyledDataGrid
rows={escalationHistory}
columns={columns}
onRowSelectionModelChange={(newRowSelectionModel) => {
setRowSelectionModel(newRowSelectionModel);
}}
/>
</Grid>
</>
)}
<Grid item xs={12}>
<FormControl>
<Controller
name="qcAccept"
control={control}
defaultValue={true}
render={({ field }) => (
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
{...field}
value={field.value?.toString() || "true"}
onChange={(e) => {
const value = e.target.value === 'true';
if (!value && Boolean(errors.acceptQty)) {
setValue("acceptQty", itemDetail.acceptedQty);
}
field.onChange(value);
}}
>
<FormControlLabel value="true" control={<Radio />} label="接受" />
<Box sx={{mr:2}}>
<TextField
type="number"
label={t("acceptQty")}
sx={{ width: '150px' }}
defaultValue={accQty}
disabled={!qcAccept}
{...register("acceptQty", {
required: "acceptQty required!",
})}
error={Boolean(errors.acceptQty)}
helperText={errors.acceptQty?.message}
/>
</Box>
<FormControlLabel value="false" control={<Radio />} label="不接受及上報" />
</RadioGroup>
)}
/>
</FormControl>
</Grid>
{/* <Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Escalation Result")}
</Typography>
</Grid>
<Grid item xs={12}>
<EscalationComponent
forSupervisor={true}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</Grid> */}
</Grid>
</Grid>
</>
);
};
export default QcFormVer2;

+ 78
- 0
src/components/Jodetail/QcSelect.tsx Прегледај датотеку

@@ -0,0 +1,78 @@
import React, { useCallback, useMemo } from "react";
import {
Autocomplete,
Box,
Checkbox,
Chip,
ListSubheader,
MenuItem,
TextField,
Tooltip,
} from "@mui/material";
import { QcItemWithChecks } from "@/app/api/qc";
import { useTranslation } from "react-i18next";

interface CommonProps {
allQcs: QcItemWithChecks[];
error?: boolean;
}

interface SingleAutocompleteProps extends CommonProps {
value: number | string | undefined;
onQcSelect: (qcItemId: number) => void | Promise<void>;
// multiple: false;
}

type Props = SingleAutocompleteProps;

const QcSelect: React.FC<Props> = ({ allQcs, value, error, onQcSelect }) => {
const { t } = useTranslation("home");
const filteredQc = useMemo(() => {
// do filtering here if any
return allQcs;
}, [allQcs]);
const options = useMemo(() => {
return [
{
value: -1, // think think sin
label: t("None"),
group: "default",
},
...filteredQc.map((q) => ({
value: q.id,
label: `${q.code} - ${q.name}`,
group: "existing",
})),
];
}, [t, filteredQc]);

const currentValue = options.find((o) => o.value === value) || options[0];

const onChange = useCallback(
(
event: React.SyntheticEvent,
newValue: { value: number; group: string } | { value: number }[],
) => {
const singleNewVal = newValue as {
value: number;
group: string;
};
onQcSelect(singleNewVal.value);
},
[onQcSelect],
);

return (
<Autocomplete
noOptionsText={t("No Qc")}
disableClearable
fullWidth
value={currentValue}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => <TextField {...params} error={error} />}
/>
);
};
export default QcSelect;

+ 243
- 0
src/components/Jodetail/SearchResultsTable.tsx Прегледај датотеку

@@ -0,0 +1,243 @@
import React, { useCallback } from 'react';
import {
Box,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Checkbox,
TextField,
TablePagination,
FormControl,
Select,
MenuItem,
} from '@mui/material';
import { useTranslation } from 'react-i18next';

interface SearchItemWithQty {
id: number;
label: string;
qty: number | null;
currentStockBalance?: number;
uomDesc?: string;
targetDate?: string | null;
groupId?: number | null;
}

interface Group {
id: number;
name: string;
targetDate: string;
}

interface SearchResultsTableProps {
items: SearchItemWithQty[];
selectedItemIds: (string | number)[];
groups: Group[];
onItemSelect: (itemId: number, checked: boolean) => void;
onQtyChange: (itemId: number, qty: number | null) => void;
onQtyBlur: (itemId: number) => void;
onGroupChange: (itemId: number, groupId: string) => void;
isItemInCreated: (itemId: number) => boolean;
pageNum: number;
pageSize: number;
onPageChange: (event: unknown, newPage: number) => void;
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const SearchResultsTable: React.FC<SearchResultsTableProps> = ({
items,
selectedItemIds,
groups,
onItemSelect,
onQtyChange,
onGroupChange,
onQtyBlur,
isItemInCreated,
pageNum,
pageSize,
onPageChange,
onPageSizeChange,
}) => {
const { t } = useTranslation("pickOrder");

// Calculate pagination
const startIndex = (pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedResults = items.slice(startIndex, endIndex);

const handleQtyChange = useCallback((itemId: number, value: string) => {
// Only allow numbers
if (value === "" || /^\d+$/.test(value)) {
const numValue = value === "" ? null : Number(value);
onQtyChange(itemId, numValue);
}
}, [onQtyChange]);

return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}>
{t("Selected")}
</TableCell>
<TableCell>
{t("Item")}
</TableCell>
<TableCell>
{t("Group")}
</TableCell>
<TableCell align="right">
{t("Current Stock")}
</TableCell>
<TableCell align="right">
{t("Stock Unit")}
</TableCell>
<TableCell align="right">
{t("Order Quantity")}
</TableCell>
<TableCell align="right">
{t("Target Date")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedResults.length === 0 ? (
<TableRow>
<TableCell colSpan={12} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedResults.map((item) => (
<TableRow key={item.id}>
<TableCell padding="checkbox">
<Checkbox
checked={selectedItemIds.includes(item.id)}
onChange={(e) => onItemSelect(item.id, e.target.checked)}
disabled={isItemInCreated(item.id)}
/>
</TableCell>
{/* Item */}
<TableCell>
<Box>
<Typography variant="body2">
{item.label.split(' - ')[1] || item.label}
</Typography>
<Typography variant="caption" color="textSecondary">
{item.label.split(' - ')[0] || ''}
</Typography>
</Box>
</TableCell>
{/* Group */}
<TableCell>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={item.groupId?.toString() || ""}
onChange={(e) => onGroupChange(item.id, e.target.value)}
displayEmpty
disabled={isItemInCreated(item.id)}
>
<MenuItem value="">
<em>{t("No Group")}</em>
</MenuItem>
{groups.map((group) => (
<MenuItem key={group.id} value={group.id.toString()}>
{group.name}
</MenuItem>
))}
</Select>
</FormControl>
</TableCell>
{/* Current Stock */}
<TableCell align="right">
<Typography
variant="body2"
color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"}
sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }}
>
{item.currentStockBalance?.toLocaleString()||0}
</Typography>
</TableCell>
{/* Stock Unit */}
<TableCell align="right">
<Typography variant="body2">
{item.uomDesc || "-"}
</Typography>
</TableCell>
<TableCell align="right">
{/* Order Quantity */}

<TextField
type="number"
size="small"
value={item.qty || ""}
onChange={(e) => {
const value = e.target.value;
// Only allow numbers
if (value === "" || /^\d+$/.test(value)) {
const numValue = value === "" ? null : Number(value);
onQtyChange(item.id, numValue);
}
}}
onBlur={() => {
// Trigger auto-add check when user finishes input (clicks elsewhere)
onQtyBlur(item.id); // ← Change this to call onQtyBlur instead!
}}
inputProps={{
style: { textAlign: 'center' }
}}
sx={{
width: '80px',
'& .MuiInputBase-input': {
textAlign: 'center',
cursor: 'text'
}
}}
disabled={isItemInCreated(item.id)}
/>
</TableCell>
{/* Target Date */}
<TableCell align="right">
<Typography variant="body2">
{item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
</Typography>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={items.length}
page={(pageNum - 1)}
rowsPerPage={pageSize}
onPageChange={onPageChange}
onRowsPerPageChange={onPageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);
};

export default SearchResultsTable;

+ 321
- 0
src/components/Jodetail/StockInFormVer2.tsx Прегледај датотеку

@@ -0,0 +1,321 @@
"use client";

import {
PurchaseQcResult,
PurchaseQCInput,
StockInInput,
} from "@/app/api/po/actions";
import {
Box,
Card,
CardContent,
Grid,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { QcItemWithChecks } from "@/app/api/qc";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import dayjs from "dayjs";
// 修改接口以支持 PickOrder 数据
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";

// change PurchaseQcResult to stock in entry props
interface Props {
itemDetail: StockInLine | (GetPickOrderLineInfo & { pickOrderCode: string });
// qc: QcItemWithChecks[];
disabled: boolean;
}
type EntryError =
| {
[field in keyof StockInInput]?: string;
}
| undefined;

// type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>;

const StockInFormVer2: React.FC<Props> = ({
// qc,
itemDetail,
disabled,
}) => {
const {
t,
i18n: { language },
} = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<StockInInput>();
// console.log(itemDetail);

useEffect(() => {
console.log("triggered");
// receiptDate default tdy
setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT));
setValue("status", "received");
}, [setValue]);

useEffect(() => {
console.log(errors);
}, [errors]);

const productionDate = watch("productionDate");
const expiryDate = watch("expiryDate");
const uom = watch("uom");

useEffect(() => {
console.log(uom);
console.log(productionDate);
console.log(expiryDate);
if (expiryDate) clearErrors();
if (productionDate) clearErrors();
}, [expiryDate, productionDate, clearErrors]);

// 检查是否为 PickOrder 数据
const isPickOrderData = 'pickOrderCode' in itemDetail;

// 获取 UOM 显示值
const getUomDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.uomDesc || pickOrderItem.uomCode || '';
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return uom?.code || stockInItem.uom?.code || '';
}
};

// 获取 Item 显示值
const getItemDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.itemCode || '';
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return stockInItem.itemNo || '';
}
};

// 获取 Item Name 显示值
const getItemNameDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.itemName || '';
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return stockInItem.itemName || '';
}
};

// 获取 Quantity 显示值
const getQuantityDisplayValue = () => {
if (isPickOrderData) {
// PickOrder 数据
const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
return pickOrderItem.requiredQty || 0;
} else {
// StockIn 数据
const stockInItem = itemDetail as StockInLine;
return stockInItem.acceptedQty || 0;
}
};

return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("stock in information")}
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemNo")}
fullWidth
{...register("itemNo", {
required: "itemNo required!",
})}
value={getItemDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("itemName")}
fullWidth
{...register("itemName", {
required: "itemName required!",
})}
value={getItemNameDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<Controller
name="productionDate"
control={control}
rules={{
required: "productionDate required!",
}}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={{ width: "100%" }}
label={t("productionDate")}
value={productionDate ? dayjs(productionDate) : undefined}
disabled={disabled}
onChange={(date) => {
console.log(date);
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("productionDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.productionDate?.message),
helperText: errors.productionDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
<Grid item xs={6}>
<Controller
name="expiryDate"
control={control}
rules={{
required: "expiryDate required!",
}}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={{ width: "100%" }}
label={t("expiryDate")}
value={expiryDate ? dayjs(expiryDate) : undefined}
disabled={disabled}
onChange={(date) => {
console.log(date);
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.expiryDate?.message),
helperText: errors.expiryDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("receivedQty")}
fullWidth
{...register("receivedQty", {
required: "receivedQty required!",
})}
value={getQuantityDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("uom")}
fullWidth
{...register("uom", {
required: "uom required!",
})}
value={getUomDisplayValue()}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("acceptedQty")}
fullWidth
{...register("acceptedQty", {
required: "acceptedQty required!",
})}
value={getQuantityDisplayValue()}
disabled={true}
// disabled={disabled}
// error={Boolean(errors.acceptedQty)}
// helperText={errors.acceptedQty?.message}
/>
</Grid>
{/* <Grid item xs={4}>
<TextField
label={t("acceptedWeight")}
fullWidth
// {...register("acceptedWeight", {
// required: "acceptedWeight required!",
// })}
disabled={disabled}
error={Boolean(errors.acceptedWeight)}
helperText={errors.acceptedWeight?.message}
/>
</Grid> */}
</Grid>
);
};
export default StockInFormVer2;

+ 24
- 0
src/components/Jodetail/TwoLineCell.tsx Прегледај датотеку

@@ -0,0 +1,24 @@
import { Box, Tooltip } from "@mui/material";
import React from "react";

const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<Tooltip title={children}>
<Box
sx={{
whiteSpace: "normal",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
lineHeight: "22px",
}}
>
{children}
</Box>
</Tooltip>
);
};

export default TwoLineCell;

+ 73
- 0
src/components/Jodetail/UomSelect.tsx Прегледај датотеку

@@ -0,0 +1,73 @@

import { ItemCombo } from "@/app/api/settings/item/actions";
import { Autocomplete, TextField } from "@mui/material";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";

interface CommonProps {
allUom: ItemCombo[];
error?: boolean;
}

interface SingleAutocompleteProps extends CommonProps {
value: number | string | undefined;
onUomSelect: (itemId: number) => void | Promise<void>;
// multiple: false;
}

type Props = SingleAutocompleteProps;

const UomSelect: React.FC<Props> = ({
allUom,
value,
error,
onUomSelect
}) => {
const { t } = useTranslation("item");
const filteredUom = useMemo(() => {
return allUom
}, [allUom])

const options = useMemo(() => {
return [
{
value: -1, // think think sin
label: t("None"),
group: "default",
},
...filteredUom.map((i) => ({
value: i.id as number,
label: i.label,
group: "existing",
})),
];
}, [t, filteredUom]);

const currentValue = options.find((o) => o.value === value) || options[0];

const onChange = useCallback(
(
event: React.SyntheticEvent,
newValue: { value: number; group: string } | { value: number }[],
) => {
const singleNewVal = newValue as {
value: number;
group: string;
};
onUomSelect(singleNewVal.value)
}
, [onUomSelect])
return (
<Autocomplete
noOptionsText={t("No Uom")}
disableClearable
fullWidth
value={currentValue}
onChange={onChange}
getOptionLabel={(option) => option.label}
options={options}
renderInput={(params) => <TextField {...params} error={error} />}
/>
);
}
export default UomSelect

+ 85
- 0
src/components/Jodetail/VerticalSearchBox.tsx Прегледај датотеку

@@ -0,0 +1,85 @@
import { Criterion } from "@/components/SearchBox/SearchBox";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { Card, CardContent, Typography, Grid, TextField, Button, Stack } from "@mui/material";
import { RestartAlt, Search } from "@mui/icons-material";
import { Autocomplete } from "@mui/material";

const VerticalSearchBox = ({ criteria, onSearch, onReset }: {
criteria: Criterion<any>[];
onSearch: (inputs: Record<string, any>) => void;
onReset?: () => void;
}) => {
const { t } = useTranslation("common");
const [inputs, setInputs] = useState<Record<string, any>>({});

const handleInputChange = (paramName: string, value: any) => {
setInputs(prev => ({ ...prev, [paramName]: value }));
};

const handleSearch = () => {
onSearch(inputs);
};

const handleReset = () => {
setInputs({});
onReset?.();
};

return (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 12, sm: 12 }}>
{criteria.map((c) => {
return (
<Grid key={c.paramName} item xs={12}>
{c.type === "text" && (
<TextField
label={t(c.label)}
fullWidth
onChange={(e) => handleInputChange(c.paramName, e.target.value)}
value={inputs[c.paramName] || ""}
/>
)}
{c.type === "autocomplete" && (
<Autocomplete
options={c.options || []}
getOptionLabel={(option: any) => option.label}
onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")}
value={c.options?.find(option => option.value === inputs[c.paramName]) || null}
renderInput={(params) => (
<TextField
{...params}
label={t(c.label)}
fullWidth
/>
)}
/>
)}
</Grid>
);
})}
</Grid>
<Stack direction="row" spacing={2} sx={{ mt: 2 }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</Stack>
</CardContent>
</Card>
);
};

export default VerticalSearchBox;

+ 78
- 0
src/components/Jodetail/dummyQcTemplate.tsx Прегледај датотеку

@@ -0,0 +1,78 @@
import { PutAwayLine } from "@/app/api/po/actions"

export interface QcData {
id: number,
qcItem: string,
qcDescription: string,
isPassed: boolean | undefined
failedQty: number | undefined
remarks: string | undefined
}

export const dummyQCData: QcData[] = [
{
id: 1,
qcItem: "包裝",
qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 2,
qcItem: "肉質",
qcDescription: "肉質鬆散,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 3,
qcItem: "顔色",
qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 4,
qcItem: "狀態",
qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
{
id: 5,
qcItem: "異物",
qcDescription: "有不屬於本食材的雜質,則不合格",
isPassed: undefined,
failedQty: undefined,
remarks: undefined,
},
]

export interface EscalationData {
id: number,
escalation: string,
supervisor: string,
}


export const dummyEscalationHistory: EscalationData[] = [
{
id: 1,
escalation: "上報1",
supervisor: "陳大文"
},
]

export const dummyPutawayLine: PutAwayLine[] = [
{
id: 1,
qty: 100,
warehouseId: 1,
warehouse: "W001 - 憶兆 3樓A倉",
printQty: 100
}
]

+ 1
- 0
src/components/Jodetail/index.ts Прегледај датотеку

@@ -0,0 +1 @@
export { default } from "./FinishedGoodSearchWrapper";

+ 5
- 0
src/components/NavigationContent/NavigationContent.tsx Прегледај датотеку

@@ -208,6 +208,11 @@ const NavigationContent: React.FC = () => {
label: "Job Order",
path: "/jo",
},
{
icon: <RequestQuote />,
label: "Job Order Pickexcution",
path: "/jodetail",
},
],
},
{


+ 15
- 1
src/i18n/zh/common.json Прегледај датотеку

@@ -89,6 +89,20 @@
"Put Away Scan": "上架掃碼",
"Finished Good Order": "成品出倉",
"finishedGood": "成品",
"Router": "執貨路線"
"Router": "執貨路線",
"Job Order Pickexcution": "工單提料",
"No data available": "沒有資料",
"Start Scan": "開始掃碼",
"Stop Scan": "停止掃碼",
"Scan Result": "掃碼結果",
"Expiry Date": "有效期",
"Pick Order Code": "提料單編號",
"Target Date": "需求日期",
"Lot Required Pick Qty": "批號需求數量",
"Job Order Match": "工單匹配",
"All Pick Order Lots": "所有提料單批號",
"Row per page": "每頁行數",
"No data available": "沒有資料",
"jodetail": "工單細節"

}

+ 58
- 0
src/i18n/zh/jo.json Прегледај датотеку

@@ -16,8 +16,14 @@
"Pending for pick": "待提料",
"Planning": "計劃中",
"Scanned": "已掃碼",
"Processing": "已開始工序",
"Storing": "入倉中",
"completed": "已完成",
"Completed": "已完成",
"Cancel": "取消",
"Scan Status": "掃碼狀態",
"Start Job Order": "開始工單",
<<<<<<< HEAD
"Target Production Date" : "預計生產日期",
"Production Priority" : "生產優先度",
"Sequence" : "序",
@@ -28,4 +34,56 @@
"Lines with sufficient stock: ": "可提料項目數量: ",
"Lines with insufficient stock: ": "未能提料項目數量: ",
"Item Name" : "原材料/半成品名稱"
=======
"Job Order Pickexcution": "工單提料",
"Pick Order Detail": "提料單細節",
"Finished Job Order Record": "已完成工單記錄",
"Index": "編號",
"Route": "路線",
"Item Code": "成品/半成品編號",
"Item Name": "成品/半成品名稱",
"Qty": "數量",
"Unit": "單位",
"Location": "位置",
"Scan Result": "掃碼結果",
"Expiry Date": "有效期",
"Target Date": "需求日期",
"Lot Required Pick Qty": "批號需求數量",
"Job Order Match": "工單對料",
"Lot No": "批號",
"Submit Required Pick Qty": "提交需求數量",
"All Pick Order Lots": "所有提料單批號",
"Row per page": "每頁行數",
"No data available": "沒有資料",
"jodetail": "工單細節",
"Start QR Scan": "開始QR掃碼",
"Stop QR Scan": "停止QR掃碼",
"Rows per page": "每頁行數",
"Job Order Item Name": "工單物料名稱",
"Job Order Code": "工單編號",
"View Details": "查看詳情",
"Required Qty": "需求數量",
"completed Job Order pick orders with Matching": "工單已完成提料和對料",
"No completed Job Order pick orders with matching found": "沒有匹配的工單",
"completed Job Order pick orders with matching": "工單已完成提料和對料",
"Total": "總數",
"Back to List": "返回列表",
"second Scan Status": "對料狀態",
"Actual Pick Qty": "實際提料數量",
"Processing Status": "處理狀態",
"Lot Availability": "批號可用性",
"Pick Order Id": "提料單編號",
"Pick Order Code": "提料單編號",
"Pick Order Conso Code": "提料單組合編號",
"Pick Order Target Date": "提料單需求日期",
"Pick Order Status": "提料單狀態",
"Second Scan Status": "對料狀態",
"Job Order Pick Order Details": "工單提料單詳情",
"Scanning...": "掃碼中",
"Unassigned Job Orders": "未分配工單",
"Please scan the item qr code": "請掃描物料二維碼",
"Please make sure the qty is enough": "請確保物料數量是足夠",
"Please make sure all required items are picked": "請確保所有物料已被提取",
"Do you want to start job order": "是否開始工單"
>>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b
}

+ 17
- 10
src/i18n/zh/pickOrder.json Прегледај датотеку

@@ -172,7 +172,7 @@
"Job Order Code": "工單編號",
"QC Check": "QC 檢查",
"QR Code Scan": "QR Code掃描",
"Pick Order Details": "提料單詳情",
"Pick Order Details": "提料單資料",
"Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。",
"Pick order completed successfully!": "提料單完成成功!",
"Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。",
@@ -252,16 +252,16 @@
"Shop Name":"商店名稱",
"Shop Address":"商店地址",
"Delivery Date":"目標日期",
"Pick Execution 2/F":"進行提料 2/F",
"Pick Execution 4/F":"進行提料 4/F",
"Pick Execution Detail":"進行提料詳情",
"Pick Execution 2/F":"取單 2/F",
"Pick Execution 4/F":"取單 4/F",
"Finished Good Detail":"成品資料",
"Submit Required Pick Qty":"提交所需提料數量",
"Scan Result":"掃描結果",
"Ticket No.":"提票號碼",
"Start QR Scan":"開始QR掃描",
"Stop QR Scan":"停止QR掃描",
"Scanning...":"掃描中...",
"Print DN/Label":"列印送貨單/標籤",
"Store ID":"儲存編號",
"QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。",
"Lot Number Mismatch":"批次號碼不符",
@@ -270,15 +270,15 @@
"Scanned Lot:":"掃描批次:",
"Confirm":"確認",
"Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次",
"Print Draft":"列印草稿",
"Print Pick Order and DN Label":"列印提料單和送貨單標貼",
"Print Pick Order":"列印提料單",
"Print DN Label":"列印送貨單標貼",
"Print Draft":"列印送貨單草稿",
"Print Pick Order and DN Label":"列印送貨單標貼",
"Print Pick Order":"列印送貨單",
"Print DN Label":"列印標貼",
"If you confirm, the system will:":"如果您確認,系統將:",
"QR code verified.":"QR 碼驗證成功。",
"Order Finished":"訂單完成",
"Submitted Status":"提交狀態",
"Pick Execution Record":"提料執行記錄",
"Finished Good Record":"已完成出倉記錄",
"Delivery No.":"送貨單編號",
"Total":"總數",
"completed DO pick orders":"已完成送貨單提料單",
@@ -288,12 +288,19 @@
"COMPLETED":"已完成",
"FG orders":"成品提料單",
"Back to List":"返回列表",
<<<<<<< HEAD

"Enter the number of cartons: ": "請輸入總箱數",
"Number of cartons": "箱數",
"You need to enter a number": "箱數不能為空",
"Number must be at least 1": "箱數最少為一",
"Printed Successfully.": "已成功列印"
=======
"No completed DO pick orders found":"沒有已完成送貨單提料單",
"Enter the number of cartons: ": "請輸入總箱數",
"Number of cartons": "箱數",
"Total exceeds required qty":"總數超出所需數量"
>>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b




Loading…
Откажи
Сачувај