CANCERYS\kw093 1 semana atrás
pai
commit
4883ead941
7 arquivos alterados com 213 adições e 93 exclusões
  1. +4
    -0
      src/app/api/dashboard/actions.ts
  2. +2
    -0
      src/app/api/po/index.ts
  3. +1
    -0
      src/app/api/settings/item/actions.ts
  4. +4
    -0
      src/app/utils/formatUtil.ts
  5. +1
    -1
      src/components/PoDetail/EscalationComponent.tsx
  6. +193
    -87
      src/components/PoDetail/PoDetail.tsx
  7. +8
    -5
      src/components/PoDetail/PoInputGrid.tsx

+ 4
- 0
src/app/api/dashboard/actions.ts Ver arquivo

@@ -30,6 +30,9 @@ export interface StockInLineEntry {
acceptedQty: number;
status?: string;
expiryDate?: string;
productLotNo?: string;
receiptDate?: string;
dnDate?: string;
}

export interface PurchaseQcResult {
@@ -87,6 +90,7 @@ export const fetchStockInLineInfo = cache(async (stockInLineId: number) => {
});

export const createStockInLine = async (data: StockInLineEntry) => {
console.log(data)
const stockInLine = await serverFetchJson<
PostStockInLiineResponse<StockInLineEntry>
>(`${BASE_API_URL}/stockInLine/create`, {


+ 2
- 0
src/app/api/po/index.ts Ver arquivo

@@ -60,6 +60,8 @@ export interface StockInLine {
poCode: string;
uom: Uom;
defaultWarehouseId: number; // id for now
dnNo: string;
dnDate: number[];
}

export const fetchPoList = cache(async (queryParams?: Record<string, any>) => {


+ 1
- 0
src/app/api/settings/item/actions.ts Ver arquivo

@@ -56,6 +56,7 @@ export interface ItemCombo {
label: string,
uomId: number,
uom: string,
group?: string,
}

export const fetchAllItemsInClient = cache(async () => {


+ 4
- 0
src/app/utils/formatUtil.ts Ver arquivo

@@ -76,6 +76,10 @@ export const dayjsToInputDateTimeString = (date: Dayjs) => {
return date.format(`${INPUT_DATE_FORMAT}T${OUTPUT_TIME_FORMAT}`);
};

export const outputDateStringToInputDateString = (date: string) => {
return dayjsToInputDateString(dateStringToDayjs(date))
}

export const stockInLineStatusMap: { [status: string]: number } = {
draft: 0,
pending: 1,


+ 1
- 1
src/components/PoDetail/EscalationComponent.tsx Ver arquivo

@@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({
];

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


+ 193
- 87
src/components/PoDetail/PoDetail.tsx Ver arquivo

@@ -28,6 +28,9 @@ import {
Typography,
Checkbox,
FormControlLabel,
Card,
CardContent,
Radio,
} from "@mui/material";
import { useTranslation } from "react-i18next";
// import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
@@ -57,7 +60,7 @@ import PoInputGrid from "./PoInputGrid";
import { QcItemWithChecks } from "@/app/api/qc";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { WarehouseResult } from "@/app/api/warehouse";
import { calculateWeight, returnWeightUnit } from "@/app/utils/formatUtil";
import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil";
import { CameraContext } from "../Cameras/CameraProvider";
import PoQcStockInModal from "./PoQcStockInModal";
import QrModal from "./QrModal";
@@ -69,6 +72,11 @@ import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { fetchPoListClient } from "@/app/api/po/actions";
import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
import { createStockInLine } from "@/app/api/dashboard/actions";
import { Controller, FormProvider, useForm } from "react-hook-form";
import dayjs, { Dayjs } from "dayjs";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers";
import { debounce } from "lodash";
//import { useRouter } from "next/navigation";


@@ -169,6 +177,11 @@ const PoSearchList: React.FC<{
);
};

interface PolInputResult {
lotNo: string,
dnQty: number,
}

const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
const cameras = useContext(CameraContext);
console.log(cameras);
@@ -178,10 +191,20 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
const [rows, setRows] = useState<PurchaseOrderLine[]>(
purchaseOrder.pol || [],
);
const [polInputList, setPolInputList] = useState<PolInputResult[]>([])
useEffect(() => {
setPolInputList(
(purchaseOrder.pol ?? []).map(() => ({
lotNo: "",
dnQty: 0,
} as PolInputResult))
);
}, [purchaseOrder.pol]);

const pathname = usePathname()
const searchParams = useSearchParams();
const [row, setRow] = useState(rows[0]);
const [selectedRow, setSelectedRow] = useState<PurchaseOrderLine | null>(null);

const [stockInLine, setStockInLine] = useState<StockInLine[]>([]);
const [processedQty, setProcessedQty] = useState(0);
@@ -191,8 +214,13 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
const [selectedPoId, setSelectedPoId] = useState(po.id);
const currentPoId = searchParams.get('id');
const selectedIdsParam = searchParams.get('selectedIds');
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);

// const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
const dnFormProps = useForm({
defaultValues: {
dnNo: '',
dnDate: dayjsToDateString(dayjs())
}
})
const fetchPoList = useCallback(async () => {
try {
if (selectedIdsParam) {
@@ -217,11 +245,12 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
const fetchPoDetail = useCallback(async (poId: string) => {
try {
const result = await fetchPoInClient(parseInt(poId));
console.log(result)
if (result) {
setPurchaseOrder(result);
setRows(result.pol || []);
if (result.pol && result.pol.length > 0) {
setRow(result.pol[0]);
setSelectedRow(result.pol[0]);
setStockInLine(result.pol[0].stockInLine);
setProcessedQty(result.pol[0].processed);
}
@@ -233,10 +262,15 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
const handlePoSelect = useCallback(
async (selectedPo: PoResult) => {
if (selectedPo.id === selectedPoId) return;
setSelectedPoId(selectedPo.id);
await fetchPoDetail(selectedPo.id.toString());
const newSelectedIds = selectedIdsParam || selectedPo.id.toString();
router.push(`/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`, { scroll: false });
// router.push(`/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`, { scroll: false });
const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`;
if (pathname + searchParams.toString() !== newUrl) {
router.replace(newUrl, { scroll: false });
}
},
[router, selectedIdsParam, fetchPoDetail]
);
@@ -246,7 +280,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
setSelectedPoId(parseInt(currentPoId));
fetchPoDetail(currentPoId);
}
}, [currentPoId, selectedPoId, fetchPoDetail]);
}, [currentPoId, fetchPoDetail]);

useEffect(() => {
fetchPoList();
@@ -257,13 +291,6 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
setSelectedPoId(parseInt(currentPoId));
}
}, [currentPoId]);







const removeParam = (paramToRemove: string) => {
const newParams = new URLSearchParams(searchParams.toString());
@@ -296,7 +323,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
function Row(props: { row: PurchaseOrderLine }) {
const { row } = props;
// const [firstReceiveQty, setFirstReceiveQty] = useState<number>()
const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
// const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
// const [open, setOpen] = useState(false);
const [processedQty, setProcessedQty] = useState(row.processed);
const [currStatus, setCurrStatus] = useState(row.status);
@@ -309,6 +336,9 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
() => returnWeightUnit(row.uom),
[row.uom],
);
const rowIndex = useMemo(() => {
return rows.findIndex((r) => r.id === row.id)
}, [])

useEffect(() => {
const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null
@@ -328,21 +358,18 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
}, [processedQty, row.qty]);

const handleRowSelect = () => {
setSelectedRowId(row.id);
setRow(row);
// setSelectedRowId(row.id);
setSelectedRow(row);
setStockInLine(row.stockInLine);
setProcessedQty(row.processed);
};
const changeStockInLines = useCallback(
(id: number) => {
console.log(id)
//rows = purchaseOrderLine
console.log(rows)
const target = rows.find((r) => r.id === id)
const stockInLine = target!.stockInLine
console.log(stockInLine)
setStockInLine(stockInLine)
setRow(target!)
setSelectedRow(target!)
// console.log(pathname)
// router.replace(`/po/edit?id=${item.poId}&polId=${item.polId}&stockInLineId=${item.stockInLineId}`);
},
@@ -355,42 +382,64 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
// post stock in line
const oldId = row.id;
const postData = {
dnNo: dnFormProps.watch("dnNo"),
dnDate: outputDateStringToInputDateString(dnFormProps.watch("dnDate")),
itemId: row.itemId,
itemNo: row.itemNo,
itemName: row.itemName,
purchaseOrderId: row.purchaseOrderId,
purchaseOrderLineId: row.id,
acceptedQty: secondReceiveQty || 0,
acceptedQty: polInputList[rowIndex].dnQty || 0,
productLotNo: polInputList[rowIndex].lotNo || '',
// acceptedQty: secondReceiveQty || 0,
// acceptedQty: row.acceptedQty,
};
if (secondReceiveQty === 0) return
// if (secondReceiveQty === 0) return
const res = await createStockInLine(postData);
if (res) {
fetchPoDetail(selectedPoId.toString());
}
console.log(res);
}, 200);
},
[],
[polInputList, row, dnFormProps],
);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;

// Allow empty input
if (raw.trim() === '') {
setSecondReceiveQty(undefined);
return;
}

// Keep digits only
const cleaned = raw.replace(/[^\d]/g, '');
if (cleaned === '') {
// If the user typed only non-digits, keep previous value
return;
}

// Parse and clamp to non-negative integer
const next = Math.max(0, Math.floor(Number(cleaned)));
setSecondReceiveQty(next);
};
const handleChange = useCallback(debounce((e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;
const id = e.target.id
const temp = polInputList
switch (id) {
case "lotNo":
if (raw.trim() === '') {
temp[rowIndex].lotNo = '';
break;
}
temp[rowIndex].lotNo = raw.trim();
break;
case "dnQty":
// Allow empty input
if (raw.trim() === '') {
temp[rowIndex].dnQty = 0;
break;
}

// Keep digits only
const cleaned = raw.replace(/[^\d]/g, '');
if (cleaned === '') {
// If the user typed only non-digits, keep previous value
break;
}

// Parse and clamp to non-negative integer
const next = Math.max(0, Math.floor(Number(cleaned)));
temp[rowIndex].dnQty = next;
break;
default:
break;
}
// setPolInputList(() => temp)
}, 300), [rowIndex]);
return (
<>
<TableRow
@@ -410,10 +459,10 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
</IconButton>
</TableCell> */}
<TableCell align="center" sx={{ width: '60px' }}>
<Checkbox
checked={selectedRowId === row.id}
onChange={handleRowSelect}
onClick={(e) => e.stopPropagation()}
<Radio
checked={selectedRow?.id === row.id}
// onChange={handleRowSelect}
// onClick={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell align="left">{row.itemNo}</TableCell>
@@ -425,16 +474,27 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
{decimalFormatter.format(totalWeight)} {weightUnit}
</TableCell> */}
{/* <TableCell align="left">{weightUnit}</TableCell> */}
<TableCell align="right">{decimalFormatter.format(row.price)}</TableCell>
{/* <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell> */}
{/* <TableCell align="left">{row.expiryDate}</TableCell> */}
<TableCell align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell>
<TableCell align="right">{integerFormatter.format(row.receivedQty)}</TableCell>
<TableCell align="center">
<TextField
id="lotNo"
label="輸入貨品批號"
type="text" // Use type="text" to allow validation in the change handler
variant="outlined"
defaultValue={polInputList[rowIndex]?.lotNo ?? ''}
onChange={handleChange}
/>
</TableCell>
<TableCell align="center">
<TextField
id="dnQty"
label="輸入來貨數量"
type="text" // Use type="text" to allow validation in the change handler
variant="outlined"
value={secondReceiveQty}
defaultValue={polInputList[rowIndex]?.dnQty ?? ''}
onChange={handleChange}
InputProps={{
inputProps: {
@@ -588,6 +648,15 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
}
}, [searchParams])

const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
if (value != null) {
const updatedValue = dayjsToDateString(value)
onChange(updatedValue)
} else {
onChange(value)
}
}, [])

return (
<>
<Stack spacing={2}>
@@ -614,39 +683,75 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {

{/* right side po info */}
<Grid item xs={8}>
<PoInfoCard po={purchaseOrder} />
{true ? (
<Stack spacing={2}>
<TextField
label={t("dnNo")}
type="text"
variant="outlined"
fullWidth
// InputProps={{
// inputProps: {
// min: 0,
// step: 1
// }
// }}
/>
<TextField
label={t("dnDate")}
type="text"
variant="outlined"
defaultValue={"11/08/2025"}
fullWidth
InputProps={{
inputProps: {
min: 0,
step: 1
}
}}
/>
<Button variant="contained" onClick={onOpenScanner} fullWidth>
提交
</Button>
</Stack>
) : undefined}
<Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
<Grid item xs={12}>
<PoInfoCard po={purchaseOrder} />
</Grid>
<Grid item xs={12}>
{true ? (
<FormProvider {...dnFormProps}>
<Stack component={"form"} spacing={2}>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={2}>
<Grid container spacing={2} sx={{ maxWidth: 'fit-content' }}>
<Grid item xs={12}>
<TextField
{...dnFormProps.register("dnNo")}
label={t("dnNo")}
type="text"
variant="outlined"
fullWidth
// InputProps={{
// inputProps: {
// min: 0,
// step: 1
// }
// }}
/>
</Grid>
<Grid item xs={12}>
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
>
<Controller
control={dnFormProps.control}
name="dnDate"
render={({ field }) => (
<DatePicker
label={t("dnDate")}
format={`${OUTPUT_DATE_FORMAT}`}
defaultValue={dateStringToDayjs(field.value)}
onChange={(newValue: Dayjs | null) => {
handleDatePickerChange(newValue, field.onChange)
}}
slotProps={{ textField: { fullWidth: true }}}
/>
)}
/>
</LocalizationProvider>
</Grid>
{/* <TextField
label={t("dnDate")}
type="text"
variant="outlined"
defaultValue={"11/08/2025"}
fullWidth
/> */}
</Grid>
{/* <Button variant="contained" onClick={onOpenScanner} fullWidth>
提交
</Button> */}
</CardContent>
</Card>
</Stack>
</FormProvider>
) : undefined}
</Grid>
</Grid>
</Grid>
</Grid>

@@ -666,9 +771,10 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
<TableCell align="right">{t("processed")}</TableCell>
<TableCell align="left">{t("uom")}</TableCell>
{/* <TableCell align="right">{t("total weight")}</TableCell> */}
<TableCell align="right">{`${t("price")} (HKD)`}</TableCell>
{/* <TableCell align="right">{`${t("price")} (HKD)`}</TableCell> */}
<TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
{renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined}
<TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
{renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}(以訂單單位計算)</TableCell> : undefined}
<TableCell align="center" sx={{ width: '100px' }}></TableCell>
</TableRow>
@@ -687,11 +793,11 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
<Grid container xs={12} justifyContent="start">
<Grid item xs={12}>
<Typography variant="h6">
已選擇: {selectedRowId ? row.itemNo : '無'} - {selectedRowId ? row.itemName : '無'}
{selectedRow ? `已選擇: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}}` : "未選擇貨品"}
</Typography>
</Grid>
<Grid item xs={12}>
{selectedRowId && (
{selectedRow && (
<TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
<Table>
<TableBody>
@@ -704,7 +810,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
stockInLine={stockInLine}
setStockInLine={setStockInLine}
setProcessedQty={setProcessedQty}
itemDetail={row}
itemDetail={selectedRow}
warehouse={warehouse}
/>
</Box>


+ 8
- 5
src/components/PoDetail/PoInputGrid.tsx Ver arquivo

@@ -39,6 +39,7 @@ import {
returnWeightUnit,
calculateWeight,
stockInLineStatusMap,
arrayToDateString,
} from "@/app/utils/formatUtil";
// import PoQcStockInModal from "./PoQcStockInModal";
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
@@ -443,17 +444,19 @@ const closeNewModal = useCallback(() => {
field: "dnNo",
headerName: t("dnNo"),
width: 125,
renderCell: () => {
return <>DN0000001</>
}
// renderCell: () => {
// return <>DN0000001</>
// }
// flex: 0.4,
},
{
field: "dnDate",
headerName: t("dnDate"),
width: 125,
renderCell: () => {
return <>07/08/2025</>
renderCell: (params) => {
console.log(params.row)
// return <>07/08/2025</>
return arrayToDateString(params.value)
}
// flex: 0.4,
},


Carregando…
Cancelar
Salvar