Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

master
CANCERYS\kw093 12 часов назад
Родитель
Сommit
d141bc0e8b
31 измененных файлов: 1197 добавлений и 70 удалений
  1. +44
    -0
      src/app/(main)/settings/qrCodeHandle/page.tsx
  2. +1
    -1
      src/app/(main)/settings/user/page.tsx
  3. +19
    -0
      src/app/api/scheduling/actions.ts
  4. +2
    -0
      src/app/api/scheduling/index.ts
  5. +1
    -0
      src/app/api/settings/printer/index.ts
  6. +2
    -0
      src/app/api/user/actions.ts
  7. +32
    -0
      src/app/api/user/client.ts
  8. +24
    -0
      src/app/api/user/index.ts
  9. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  10. +52
    -3
      src/components/DetailedSchedule/DetailedScheduleSearchView.tsx
  11. +2
    -3
      src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx
  12. +1
    -0
      src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx
  13. +9
    -2
      src/components/DetailedScheduleDetail/ViewByFGDetails.tsx
  14. +25
    -13
      src/components/EditUser/EditUser.tsx
  15. +74
    -31
      src/components/EditUser/UserDetail.tsx
  16. +11
    -5
      src/components/NavigationContent/NavigationContent.tsx
  17. +2
    -2
      src/components/ScheduleTable/ScheduleTable.tsx
  18. +43
    -9
      src/components/UserSearch/UserSearch.tsx
  19. +1
    -0
      src/components/UserSearch/index.ts
  20. +3
    -0
      src/components/qrCodeHandles/index.ts
  21. +156
    -0
      src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx
  22. +17
    -0
      src/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper.tsx
  23. +507
    -0
      src/components/qrCodeHandles/qrCodeHandleSearch.tsx
  24. +40
    -0
      src/components/qrCodeHandles/qrCodeHandleSearchLoading.tsx
  25. +21
    -0
      src/components/qrCodeHandles/qrCodeHandleSearchWrapper.tsx
  26. +66
    -0
      src/components/qrCodeHandles/qrCodeHandleTabs.tsx
  27. +29
    -0
      src/i18n/zh/common.json
  28. +1
    -0
      src/i18n/zh/jo.json
  29. +5
    -1
      src/i18n/zh/user.json
  30. +3
    -0
      src/main/java/com/ffii/fpsms/modules/user/req/UpdateUserReq.java
  31. +3
    -0
      src/main/java/com/ffii/fpsms/modules/user/web/UserController.java

+ 44
- 0
src/app/(main)/settings/qrCodeHandle/page.tsx Просмотреть файл

@@ -0,0 +1,44 @@
import { Suspense } from "react";
import { Metadata } from "next";
import Typography from "@mui/material/Typography";
import { getServerI18n } from "@/i18n";
import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper";
import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper";
import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs";
import { I18nProvider } from "@/i18n";
import Box from "@mui/material/Box";

export const metadata: Metadata = { title: "QR Code Handle" };

const QrCodeHandlePage: React.FC = async () => {
const { t } = await getServerI18n("common");
return (
<Box sx={{ width: "100%" }}>
<Typography variant="h5" sx={{ mb: 3 }}>
{t("QR Code Handle")}
</Typography>
<I18nProvider namespaces={["common", "user"]}>
<QrCodeHandleTabs
userTabContent={
<Suspense fallback={<QrCodeHandleSearchWrapper.Loading />}>
<I18nProvider namespaces={["user", "common", "dashboard"]}>
<QrCodeHandleSearchWrapper />
</I18nProvider>
</Suspense>
}
equipmentTabContent={
<Suspense fallback={<QrCodeHandleEquipmentSearchWrapper.Loading />}>
<I18nProvider namespaces={["common", "project", "dashboard"]}>
<QrCodeHandleEquipmentSearchWrapper />
</I18nProvider>
</Suspense>
}
/>
</I18nProvider>
</Box>
);
};

export default QrCodeHandlePage;

+ 1
- 1
src/app/(main)/settings/user/page.tsx Просмотреть файл

@@ -34,7 +34,7 @@ const User: React.FC = async () => {
{t("Create User")}
</Button>
</Stack>
<I18nProvider namespaces={["user", "common"]}>
<I18nProvider namespaces={["user", "common", "dashboard"]}>
<Suspense fallback={<UserSearch.Loading />}>
<UserSearch />
</Suspense>


+ 19
- 0
src/app/api/scheduling/actions.ts Просмотреть файл

@@ -177,6 +177,25 @@ export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) =>
return response;
})

export const exportProdSchedule = async (token: string | null) => {
if (!token) throw new Error("No access token found");

const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, {
method: "POST",
headers: {
"Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Authorization": `Bearer ${token}`
}
});

if (!response.ok) throw new Error(`Backend error: ${response.status}`);

const arrayBuffer = await response.arrayBuffer();
// Convert to Base64 so Next.js can send it safely over the wire
return Buffer.from(arrayBuffer).toString('base64');
};

export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => {
const response = serverFetchJson<SaveProdScheduleResponse>(
`${BASE_API_URL}/productionSchedule/detail/detailed/save`,


+ 2
- 0
src/app/api/scheduling/index.ts Просмотреть файл

@@ -105,6 +105,8 @@ export interface DetailedProdScheduleLineResult {
stockQty: number; // Warehouse stock quantity
daysLeft: number; // Days remaining before stockout
needNoOfJobOrder: number;
prodQty: number;
outputQty: number;
}

export interface DetailedProdScheduleLineBomMaterialResult {


+ 1
- 0
src/app/api/settings/printer/index.ts Просмотреть файл

@@ -9,6 +9,7 @@ export interface PrinterCombo {
label?: string;
code?: string;
name?: string;
type?: string;
description?: string;
ip?: string;
port?: number;


+ 2
- 0
src/app/api/user/actions.ts Просмотреть файл

@@ -14,9 +14,11 @@ import { cache } from "react";
export interface UserInputs {
username: string;
// name: string;
staffNo?: string;
addAuthIds?: number[];
removeAuthIds?: number[];
password?: string;
confirmPassword?: string;
}

export interface PasswordInputs {


+ 32
- 0
src/app/api/user/client.ts Просмотреть файл

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

import { NEXT_PUBLIC_API_URL } from "@/config/api";

export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => {

const token = localStorage.getItem("accessToken");
const response = await fetch(`${NEXT_PUBLIC_API_URL}/user/export-qrcode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
},
body: JSON.stringify({ userIds }),
});

if (!response.ok) {
if (response.status === 401) {
throw new Error("Unauthorized: Please log in again");
}
throw new Error(`Failed to export QR code: ${response.status} ${response.statusText}`);
}

const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "user_qrcode.pdf";
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const blobValue = new Uint8Array(arrayBuffer);

return { blobValue, filename };
};

+ 24
- 0
src/app/api/user/index.ts Просмотреть файл

@@ -7,6 +7,7 @@ export interface UserResult {
action: any;
id: number;
username: string;
staffNo: number;
// name: string;
}

@@ -71,3 +72,26 @@ export const fetchEscalationCombo = cache(async () => {
next: { tags: ["escalationCombo"]}
})
})


export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => {
const response = await fetch(`${BASE_API_URL}/user/export-qrcode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userIds }),
});

if (!response.ok) {
throw new Error("Failed to export QR code");
}

const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "user_qrcode.pdf";

const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const blobValue = new Uint8Array(arrayBuffer);

return { blobValue, filename };
};

+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Просмотреть файл

@@ -14,6 +14,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/tasks": "Task Template",
"/tasks/create": "Create Task Template",
"/settings/qcItem": "Qc Item",
"/settings/qrCodeHandle": "QR Code Handle",
"/settings/rss": "Demand Forecast Setting",
"/scheduling/rough": "Demand Forecast",
"/scheduling/rough/edit": "FG & Material Demand Forecast Detail",


+ 52
- 3
src/components/DetailedSchedule/DetailedScheduleSearchView.tsx Просмотреть файл

@@ -12,6 +12,7 @@ import {
SearchProdSchedule,
fetchDetailedProdSchedules,
fetchProdSchedules,
exportProdSchedule,
testDetailedSchedule,
} from "@/app/api/scheduling/actions";
import { defaultPagingController } from "../SearchResults/SearchResults";
@@ -21,6 +22,7 @@ import { orderBy, uniqBy, upperFirst } from "lodash";
import { Button, Stack } from "@mui/material";
import isToday from 'dayjs/plugin/isToday';
import useUploadContext from "../UploadProvider/useUploadContext";
import { FileDownload, CalendarMonth } from "@mui/icons-material";
dayjs.extend(isToday);

// may need move to "index" or "actions"
@@ -298,21 +300,68 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
}
}, [inputs])

const exportProdScheduleClick = async () => {
try {
const token = localStorage.getItem("accessToken");
// 1. Get Base64 string from server
const base64String = await exportProdSchedule(token);
// 2. Convert Base64 back to Blob
const byteCharacters = atob(base64String);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});

// 3. Trigger download (same as before)
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "production_schedule.xlsx";
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
alert("Export failed. Check the console for details.");
}
};
return (
<>
<Stack
direction="row"
justifyContent="flex-end"
flexWrap="wrap"
rowGap={2}
spacing={2} // This provides consistent space between buttons
sx={{ mb: 3 }} // Adds some margin below the button group
>
<Button
variant="contained"
variant="outlined" // Outlined variant makes it look distinct from the primary action
color="primary"
startIcon={<CalendarMonth />}
onClick={testDetailedScheduleClick}
// disabled={filteredSchedules.some(ele => arrayToDayjs(ele.scheduleAt).isToday())}
>
{t("Detailed Schedule")}
</Button>
<Button
variant="contained" // Solid button for the "Export" action
color="success" // Green color often signifies a successful action/download
startIcon={<FileDownload />}
onClick={exportProdScheduleClick}
sx={{
boxShadow: 2,
'&:hover': { backgroundColor: 'success.dark', boxShadow: 4 }
}}
>
{t("Export Schedule")}
</Button>
</Stack>
<SearchBox
criteria={searchCriteria}


+ 2
- 3
src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx Просмотреть файл

@@ -194,8 +194,7 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
}
}, [scheduleId, setIsUploading, t, router]);
// --------------------------------------------------------------------


const [tempValue, setTempValue] = useState<string | number | null>(null)
const onEditClick = useCallback((rowId: number) => {
const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId)
@@ -298,7 +297,7 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
onClick={onGlobalReleaseClick}
disabled={!scheduleId} // Disable if we don't have a schedule ID
>
{t("放單(自動生成工單)")}
{t("生成工單")}
</Button>
{/* ------------------------------------------- */}



+ 1
- 0
src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx Просмотреть файл

@@ -32,6 +32,7 @@ const DetailedScheduleDetailWrapper: React.FC<Props> & SubComponents = async ({
stockQty: line.stockQty || 0,
daysLeft: line.daysLeft || 0,
needNoOfJobOrder: line.needNoOfJobOrder || 0,
outputQty: line.outputQty || 0,
})).sort((a, b) => b.priority - a.priority);
}



+ 9
- 2
src/components/DetailedScheduleDetail/ViewByFGDetails.tsx Просмотреть файл

@@ -108,9 +108,16 @@ const ViewByFGDetails: React.FC<Props> = ({
style: { textAlign: "right" } as any,
renderCell: (row) => <>{row.daysLeft ?? 0}</>,
},
{
field: "outputQty",
label: t("每批次生產數"),
type: "read-only",
style: { textAlign: "right", fontWeight: "bold" } as any,
renderCell: (row) => <>{row.outputQty ?? 0}</>,
},
{
field: "needNoOfJobOrder",
label: t("生產量"),
label: t("生產批次"),
type: "read-only",
style: { textAlign: "right", fontWeight: "bold" } as any,
renderCell: (row) => <>{row.needNoOfJobOrder ?? 0}</>,
@@ -137,7 +144,7 @@ const ViewByFGDetails: React.FC<Props> = ({
isEditable={true}
isEdit={isEdit}
hasCollapse={true}
// Note: onReleaseClick is NOT passed here to hide the row-level "Release" function
onReleaseClick={onReleaseClick}
onEditClick={onEditClick}
handleEditChange={handleEditChange}
onSaveClick={onSaveClick}


+ 25
- 13
src/components/EditUser/EditUser.tsx Просмотреть файл

@@ -47,7 +47,7 @@ interface Props {
auths: auth[];
}

const EditUser: React.FC<Props> = async ({ user, rules, auths }) => {
const EditUser: React.FC<Props> = ({ user, rules, auths }) => {
console.log(user);
const { t } = useTranslation("user");
const formProps = useForm<UserInputs>();
@@ -73,28 +73,31 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => {

const errors = formProps.formState.errors;

const resetForm = React.useCallback(() => {
const resetForm = React.useCallback((e?: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault();
e?.stopPropagation();
console.log("triggerred");
console.log(addAuthIds);
try {
formProps.reset({
username: user.username,
// name: user.name,
// email: user.email,
staffNo: user.staffNo?.toString() ??"",
addAuthIds: addAuthIds,
removeAuthIds: [],
password: "",
confirmPassword: "",
});
formProps.clearErrors();
console.log(formProps.formState.defaultValues);
} catch (error) {
console.log(error);
setServerError(t("An error has occurred. Please try again later."));
}
}, [auths, user]);
}, [formProps, auths, user, addAuthIds, t]);

useEffect(() => {
resetForm();
}, []);
}, [user.id]);

const hasErrorsInTab = (
tabIndex: number,
@@ -146,6 +149,7 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => {
}
const userData = {
username: data.username,
staffNo: data.staffNo,
// name: user.name,
locked: false,
addAuthIds: data.addAuthIds || [],
@@ -218,17 +222,25 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => {
{tabIndex == 0 && <UserDetail />}
{tabIndex === 1 && <AuthAllocation auths={auths!} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={resetForm}
>
{t("Reset")}
</Button>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
resetForm(e);
}}
type="button"
>
{t("Reset")}
</Button>


<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
type="button"
>
{t("Cancel")}
</Button>


+ 74
- 31
src/components/EditUser/UserDetail.tsx Просмотреть файл

@@ -20,8 +20,14 @@ const UserDetail: React.FC = () => {
register,
formState: { errors },
control,
watch,
} = useFormContext<UserInputs>();

const password = watch("password");
const confirmPassword = watch("confirmPassword");
const username = watch("username");
const staffNo = watch("staffNo");

return (
<Card>
<CardContent component={Stack} spacing={4}>
@@ -33,35 +39,55 @@ const UserDetail: React.FC = () => {
<TextField
label={t("username")}
fullWidth
variant="filled"
InputLabelProps={{
shrink: !!username,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: { paddingTop: "8px" },
}}
{...register("username", {
required: "username required!",
})}
error={Boolean(errors.username)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("staffNo")}
fullWidth
variant="filled"
InputLabelProps={{
shrink: !!staffNo,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: { paddingTop: "8px" },
}}
{...register("staffNo")}
error={Boolean(errors.staffNo)}
helperText={
Boolean(errors.staffNo) && errors.staffNo?.message
? t(errors.staffNo.message)
: ""
}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("password")}
fullWidth
type="password"
variant="filled"
InputLabelProps={{
shrink: !!password,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: { paddingTop: "8px" },
}}
{...register("password")}
// helperText={
// Boolean(errors.password) &&
// (errors.password?.message
// ? t(errors.password.message)
// :
// (<>
// - 8-20 characters
// <br/>
// - Uppercase letters
// <br/>
// - Lowercase letters
// <br/>
// - Numbers
// <br/>
// - Symbols
// </>)
// )
// }
helperText={
Boolean(errors.password) &&
(errors.password?.message
@@ -71,6 +97,36 @@ const UserDetail: React.FC = () => {
error={Boolean(errors.password)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Confirm Password")}
fullWidth
type="password"
variant="filled"
InputLabelProps={{
shrink: !!confirmPassword,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: { paddingTop: "8px" },
}}
{...register("confirmPassword", {
validate: (value) => {
if (password && value !== password) {
return "Passwords do not match";
}
return true;
},
})}
error={Boolean(errors.confirmPassword)}
helperText={
Boolean(errors.confirmPassword) &&
(errors.confirmPassword?.message
? t(errors.confirmPassword.message)
: "")
}
/>
</Grid>
{/* <Grid item xs={6}>
<TextField
label={t("name")}
@@ -89,16 +145,3 @@ const UserDetail: React.FC = () => {

export default UserDetail;

{
/* <>
- 8-20 characters
<br/>
- Uppercase letters
<br/>
- Lowercase letters
<br/>
- Numbers
<br/>
- Symbols
</> */
}

+ 11
- 5
src/components/NavigationContent/NavigationContent.tsx Просмотреть файл

@@ -17,6 +17,7 @@ import Assignment from "@mui/icons-material/Assignment";
import Settings from "@mui/icons-material/Settings";
import Analytics from "@mui/icons-material/Analytics";
import Payments from "@mui/icons-material/Payments";
import QrCodeIcon from "@mui/icons-material/QrCode";
import { useTranslation } from "react-i18next";
import Typography from "@mui/material/Typography";
import { usePathname } from "next/navigation";
@@ -268,11 +269,11 @@ const NavigationContent: React.FC = () => {
label: "Demand Forecast Setting",
path: "/settings/rss",
},
{
icon: <RequestQuote />,
label: "Equipment Type",
path: "/settings/equipmentType",
},
//{
// icon: <RequestQuote />,
// label: "Equipment Type",
// path: "/settings/equipmentType",
//},
{
icon: <RequestQuote />,
label: "Equipment",
@@ -308,6 +309,11 @@ const NavigationContent: React.FC = () => {
label: "QC Check Template",
path: "/settings/user",
},
{
icon: <QrCodeIcon />,
label: "QR Code Handle",
path: "/settings/qrCodeHandle",
},
// {
// icon: <RequestQuote />,
// label: "Mail",


+ 2
- 2
src/components/ScheduleTable/ScheduleTable.tsx Просмотреть файл

@@ -227,7 +227,7 @@ function ScheduleTable<T extends ResultWithId>({
return (
<>
<TableRow hover tabIndex={-1} key={row.id}>
{/*isDetailedType(type) && (
{isDetailedType(type) && (
<TableCell>
<IconButton
color="primary"
@@ -241,7 +241,7 @@ function ScheduleTable<T extends ResultWithId>({
<PlayCircleOutlineIcon />
</IconButton>
</TableCell>
)*/}
)}
{(isEditable || hasCollapse) && (
<TableCell>
{editingRowId === row.id ? (


+ 43
- 9
src/components/UserSearch/UserSearch.tsx Просмотреть файл

@@ -8,8 +8,11 @@ import EditNote from "@mui/icons-material/EditNote";
import DeleteIcon from "@mui/icons-material/Delete";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import useUploadContext from "../UploadProvider/useUploadContext";
import { downloadFile } from "@/app/utils/commonUtil";
import { UserResult } from "@/app/api/user";
import { deleteUser } from "@/app/api/user/actions";
import QrCodeIcon from "@mui/icons-material/QrCode";
import UserSearchLoading from "./UserSearchLoading";

interface Props {
@@ -22,7 +25,12 @@ type SearchParamNames = keyof SearchQuery;
const UserSearch: React.FC<Props> = ({ users }) => {
const { t } = useTranslation("user");
const [filteredUser, setFilteredUser] = useState(users);
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
});
const router = useRouter();
const { setIsUploading } = useUploadContext();

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
@@ -31,6 +39,11 @@ const UserSearch: React.FC<Props> = ({ users }) => {
paramName: "username",
type: "text",
},
{
label: t("staffNo"),
paramName: "staffNo",
type: "text",
},
],
[t],
);
@@ -43,6 +56,20 @@ const UserSearch: React.FC<Props> = ({ users }) => {
[router, t],
);

{/*
const printQrcode = useCallback(async (lotLineId: number) => {
setIsUploading(true);
// const postData = { stockInLineIds: [42,43,44] };
const postData: LotLineToQrcode = {
inventoryLotLineId: lotLineId
}
const response = await fetchQrCodeByLotLineId(postData);
if (response) {
downloadFile(new Uint8Array(response.blobValue), response.filename!);
}
setIsUploading(false);
}, [setIsUploading]);
*/}
const onDeleteClick = useCallback((users: UserResult) => {
deleteDialog(async () => {
await deleteUser(users.id);
@@ -50,6 +77,9 @@ const UserSearch: React.FC<Props> = ({ users }) => {
}, t);
}, []);



const columns = useMemo<Column<UserResult>[]>(
() => [
{
@@ -59,6 +89,7 @@ const UserSearch: React.FC<Props> = ({ users }) => {
buttonIcon: <EditNote />,
},
{ name: "username", label: t("Username") },
{ name: "staffNo", label: t("staffNo") },
{
name: "action",
label: t("Delete"),
@@ -75,20 +106,23 @@ const UserSearch: React.FC<Props> = ({ users }) => {
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredUser(
// users.filter(
// (t) =>
// t.name.toLowerCase().includes(query.name.toLowerCase()) &&
// t.code.toLowerCase().includes(query.code.toLowerCase()) &&
// t.description.toLowerCase().includes(query.description.toLowerCase())
// )
// )
setFilteredUser(
users.filter((user) => {
const usernameMatch = !query.username ||
user.username.toLowerCase().includes(query.username.toLowerCase());
const staffNoMatch = !query.staffNo ||
String(user.staffNo).includes(String(query.staffNo));
return usernameMatch && staffNoMatch;
})
);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
}}
/>
<SearchResults<UserResult>
items={filteredUser}
columns={columns}
pagingController={{ pageNum: 1, pageSize: 10 }}
pagingController={pagingController}
setPagingController={setPagingController}
/>
</>
);


+ 1
- 0
src/components/UserSearch/index.ts Просмотреть файл

@@ -1 +1,2 @@
export { default } from "./UserSearchWrapper";


+ 3
- 0
src/components/qrCodeHandles/index.ts Просмотреть файл

@@ -0,0 +1,3 @@
export { default } from "./qrCodeHandleSearchWrapper";
export { default as QrCodeHandleSearch } from "./qrCodeHandleSearch";
export { default as QrCodeHandleEquipmentSearch } from "./qrCodeHandleEquipmentSearch";

+ 156
- 0
src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx Просмотреть файл

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

import { useCallback, useEffect, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { EquipmentResult } from "@/app/api/settings/equipment";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import { EditNote } from "@mui/icons-material";
import { useRouter } from "next/navigation";
import { GridDeleteIcon } from "@mui/x-data-grid";
import axiosInstance from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";

type Props = {
equipments: EquipmentResult[];
};

type SearchQuery = Partial<Omit<EquipmentResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const QrCodeHandleEquipmentSearch: React.FC<Props> = ({ equipments }) => {
const [filteredEquipments, setFilteredEquipments] =
useState<EquipmentResult[]>([]);
const { t } = useTranslation("common");
const router = useRouter();
const [filterObj, setFilterObj] = useState({});
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
});
const [totalCount, setTotalCount] = useState(0);
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => {
const searchCriteria: Criterion<SearchParamNames>[] = [
{ label: t("Code"), paramName: "code", type: "text" },
{ label: t("Description"), paramName: "description", type: "text" },
];
return searchCriteria;
}, [t, equipments]);

const onDetailClick = useCallback(
(equipment: EquipmentResult) => {
router.push(`/settings/equipment/edit?id=${equipment.id}`);
},
[router],
);

const onDeleteClick = useCallback(
(equipment: EquipmentResult) => {},
[router],
);

const columns = useMemo<Column<EquipmentResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onDetailClick,
buttonIcon: <EditNote />,
},
{
name: "code",
label: t("Code"),
},
{
name: "equipmentTypeId",
label: t("Equipment Type"),
sx: {minWidth: 180},
},
{
name: "description",
label: t("Description"),
},
{
name: "action",
label: t(""),
buttonIcon: <GridDeleteIcon />,
onClick: onDeleteClick,
},
],
[filteredEquipments],
);

interface ApiResponse<T> {
records: T[];
total: number;
}

const refetchData = useCallback(
async (filterObj: SearchQuery, pageNum: number, pageSize: number) => {
const authHeader = axiosInstance.defaults.headers["Authorization"];
if (!authHeader) {
setTimeout(() => {
refetchData(filterObj, pageNum, pageSize);
}, 10);
return;
}
const params = {
pageNum: pageNum,
pageSize: pageSize,
...filterObj,
};
try {
const response = await axiosInstance.get<ApiResponse<EquipmentResult>>(
`${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`,
{ params },
);
console.log(response);
if (response.status == 200) {
setFilteredEquipments(response.data.records);
setTotalCount(response.data.total);
return response;
} else {
throw "400";
}
} catch (error) {
console.error("Error fetching equipment types:", error);
throw error;
}
},
[],
);

useEffect(() => {
refetchData(filterObj, pagingController.pageNum, pagingController.pageSize);
}, [filterObj, pagingController.pageNum, pagingController.pageSize]);

const onReset = useCallback(() => {
setFilterObj({});
setPagingController({ pageNum: 1, pageSize: 10 });
}, []);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilterObj({
...query,
});
}}
onReset={onReset}
/>
<SearchResults<EquipmentResult>
items={filteredEquipments}
columns={columns}
setPagingController={setPagingController}
pagingController={pagingController}
totalCount={totalCount}
isAutoPaging={false}
/>
</>
);
};

export default QrCodeHandleEquipmentSearch;

+ 17
- 0
src/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper.tsx Просмотреть файл

@@ -0,0 +1,17 @@
import React from "react";
import QrCodeHandleEquipmentSearch from "./qrCodeHandleEquipmentSearch";
import EquipmentSearchLoading from "../EquipmentSearch/EquipmentSearchLoading";
import { fetchAllEquipments } from "@/app/api/settings/equipment";

interface SubComponents {
Loading: typeof EquipmentSearchLoading;
}

const QrCodeHandleEquipmentSearchWrapper: React.FC & SubComponents = async () => {
const equipments = await fetchAllEquipments();
return <QrCodeHandleEquipmentSearch equipments={equipments} />;
};

QrCodeHandleEquipmentSearchWrapper.Loading = EquipmentSearchLoading;

export default QrCodeHandleEquipmentSearchWrapper;

+ 507
- 0
src/components/qrCodeHandles/qrCodeHandleSearch.tsx Просмотреть файл

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

import SearchBox, { Criterion } from "../SearchBox";
import { useCallback, useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import DeleteIcon from "@mui/icons-material/Delete";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import useUploadContext from "../UploadProvider/useUploadContext";
import { downloadFile } from "@/app/utils/commonUtil";
import { UserResult } from "@/app/api/user";
import { deleteUser } from "@/app/api/user/actions";
import QrCodeIcon from "@mui/icons-material/QrCode";
import { exportUserQrCode } from "@/app/api/user/client";
import {
Checkbox,
Box,
Button,
TextField,
Stack,
Autocomplete,
Modal,
Card,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Typography
} from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
import PrintIcon from "@mui/icons-material/Print";
import CloseIcon from "@mui/icons-material/Close";
import { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
users: UserResult[];
printerCombo: PrinterCombo[];
}

type SearchQuery = Partial<Omit<UserResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const QrCodeHandleSearch: React.FC<Props> = ({ users, printerCombo }) => {
const { t } = useTranslation("user");
const [filteredUser, setFilteredUser] = useState(users);
const router = useRouter();
const { setIsUploading } = useUploadContext();
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
});

const [checkboxIds, setCheckboxIds] = useState<number[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [printQty, setPrintQty] = useState(1);

const [previewOpen, setPreviewOpen] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);

const [selectedUsersModalOpen, setSelectedUsersModalOpen] = useState(false);

const filteredPrinters = useMemo(() => {
return printerCombo.filter((printer) => {
return printer.type === "A4";
});
}, [printerCombo]);

const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
filteredPrinters.length > 0 ? filteredPrinters[0] : undefined
);

useEffect(() => {
if (!selectedPrinter || !filteredPrinters.find(p => p.id === selectedPrinter.id)) {
setSelectedPrinter(filteredPrinters.length > 0 ? filteredPrinters[0] : undefined);
}
}, [filteredPrinters, selectedPrinter]);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("Username"),
paramName: "username",
type: "text",
},
{
label: t("staffNo"),
paramName: "staffNo",
type: "text",
},
],
[t],
);

const onDeleteClick = useCallback((user: UserResult) => {
deleteDialog(async () => {
await deleteUser(user.id);
successDialog(t("Delete Success"), t);
}, t);
}, [t]);

const handleSelectUser = useCallback((userId: number, checked: boolean) => {
if (checked) {
setCheckboxIds(prev => [...prev, userId]);
} else {
setCheckboxIds(prev => prev.filter(id => id !== userId));
setSelectAll(false);
}
}, []);

const handleSelectAll = useCallback((checked: boolean) => {
if (checked) {
setCheckboxIds(filteredUser.map(user => user.id));
setSelectAll(true);
} else {
setCheckboxIds([]);
setSelectAll(false);
}
}, [filteredUser]);


const showPdfPreview = useCallback(async (userIds: number[]) => {
if (userIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportUserQrCode(userIds);
const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
setPreviewUrl(`${url}#toolbar=0`);
setPreviewOpen(true);
} catch (error) {
console.error("Error exporting QR code:", error);
} finally {
setIsUploading(false);
}
}, [setIsUploading]);


const handleClosePreview = useCallback(() => {
setPreviewOpen(false);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
}, [previewUrl]);

const handleDownloadQrCode = useCallback(async (userIds: number[]) => {
if (userIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportUserQrCode(userIds);
downloadFile(response.blobValue, response.filename);
setSelectedUsersModalOpen(false);
successDialog("二維碼已下載", t);
} catch (error) {
console.error("Error exporting QR code:", error);
} finally {
setIsUploading(false);
}
}, [setIsUploading, t]);

const handlePrint = useCallback(async () => {
if (checkboxIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportUserQrCode(checkboxIds);
const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.onload = () => {
for (let i = 0; i < printQty; i++) {
setTimeout(() => {
printWindow.print();
}, i * 500);
}
};
}
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
setSelectedUsersModalOpen(false);
successDialog("二維碼已列印", t);
} catch (error) {
console.error("Error printing QR code:", error);
} finally {
setIsUploading(false);
}
}, [checkboxIds, printQty, setIsUploading, t]);

const handleViewSelectedQrCodes = useCallback(() => {
if (checkboxIds.length === 0) {
return;
}
setSelectedUsersModalOpen(true);
}, [checkboxIds]);

const selectedUsers = useMemo(() => {
return users.filter(user => checkboxIds.includes(user.id));
}, [users, checkboxIds]);

const handleCloseSelectedUsersModal = useCallback(() => {
setSelectedUsersModalOpen(false);
}, []);

const columns = useMemo<Column<UserResult>[]>(
() => [
{
name: "id",
label: "",
renderCell: (params) => (
<Checkbox
checked={checkboxIds.includes(params.id)}
onChange={(e) => handleSelectUser(params.id, e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
),
},
{
name: "username",
label: t("Username"),
align: "left",
headerAlign: "left",
},
{
name: "staffNo",
label: t("staffNo"),
align: "left",
headerAlign: "left",
},
{
name: "staffNo",
label: t("qrcode"),
align: "center",
headerAlign: "center",
onClick: async (user: UserResult) => {
await showPdfPreview([user.id]);
},
buttonIcon: <QrCodeIcon />,
},
],
[t, checkboxIds, handleSelectUser, showPdfPreview],
);

const onReset = useCallback(() => {
setFilteredUser(users);
}, [users]);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredUser(
users.filter((user) => {
const usernameMatch = !query.username ||
user.username?.toLowerCase().includes(query.username?.toLowerCase() || "");
const staffNoMatch = !query.staffNo ||
user.staffNo?.toString().includes(query.staffNo?.toString() || "");
return usernameMatch && staffNoMatch;
})
);
setPagingController({ pageNum: 1, pageSize: 10 });
}}
onReset={onReset}
/>
<SearchResults<UserResult>
items={filteredUser}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={filteredUser.length}
isAutoPaging={true}
/>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
variant="outlined"
onClick={() => handleSelectAll(!selectAll)}
startIcon={<Checkbox checked={selectAll} />}
>
選擇全部用戶 ({checkboxIds.length} / {filteredUser.length})
</Button>
<Button
variant="contained"
onClick={handleViewSelectedQrCodes}
disabled={checkboxIds.length === 0}
color="primary"
>
查看已選擇用戶二維碼 ({checkboxIds.length})
</Button>
</Box>

<Modal
open={selectedUsersModalOpen}
onClose={handleCloseSelectedUsersModal}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Card
sx={{
position: 'relative',
width: '90%',
maxWidth: '800px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
outline: 'none',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 2,
borderBottom: 1,
borderColor: 'divider',
}}
>
<Typography variant="h6" component="h2">
已選擇用戶 ({selectedUsers.length})
</Typography>
<IconButton onClick={handleCloseSelectedUsersModal}>
<CloseIcon />
</IconButton>
</Box>

<Box
sx={{
flex: 1,
overflow: 'auto',
p: 2,
}}
>
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>
<strong>{t("Username")}</strong>
</TableCell>
<TableCell>
<strong>{t("staffNo")}</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{selectedUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={2} align="center">
沒有選擇的用戶
</TableCell>
</TableRow>
) : (
selectedUsers.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username || '-'}</TableCell>
<TableCell>{user.staffNo || '-'}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Box>

<Box
sx={{
p: 2,
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.paper',
}}
>
<Stack direction="row" justifyContent="flex-end" alignItems="center" gap={2}>
<Autocomplete<PrinterCombo>
options={filteredPrinters}
value={selectedPrinter ?? null}
onChange={(event, value) => {
setSelectedPrinter(value ?? undefined);
}}
getOptionLabel={(option) => option.name || option.label || option.code || String(option.id)}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="列印機"
sx={{ width: 300 }}
/>
)}
/>
<TextField
variant="outlined"
label="列印數量"
type="number"
value={printQty}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setPrintQty(Math.max(1, value));
}}
inputProps={{ min: 1 }}
sx={{ width: 120 }}
/>
<Button
variant="contained"
startIcon={<PrintIcon />}
onClick={handlePrint}
disabled={checkboxIds.length === 0 || filteredPrinters.length === 0}
color="primary"
>
列印
</Button>
<Button
variant="contained"
startIcon={<DownloadIcon />}
onClick={() => handleDownloadQrCode(checkboxIds)}
disabled={checkboxIds.length === 0}
color="primary"
>
下載QR碼
</Button>
</Stack>
</Box>
</Card>
</Modal>

<Modal
open={previewOpen}
onClose={handleClosePreview}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Card
sx={{
position: 'relative',
width: '90%',
maxWidth: '900px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
outline: 'none',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
p: 2,
borderBottom: 1,
borderColor: 'divider',
}}
>
<IconButton
onClick={handleClosePreview}
>
<CloseIcon />
</IconButton>
</Box>

<Box
sx={{
flex: 1,
overflow: 'auto',
p: 2,
}}
>
{previewUrl && (
<iframe
src={previewUrl}
width="100%"
height="600px"
style={{
border: 'none',
}}
title="PDF Preview"
/>
)}
</Box>
</Card>
</Modal>
</>
);
};

export default QrCodeHandleSearch;

+ 40
- 0
src/components/qrCodeHandles/qrCodeHandleSearchLoading.tsx Просмотреть файл

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const qrCodeHandleSearchLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default qrCodeHandleSearchLoading;

+ 21
- 0
src/components/qrCodeHandles/qrCodeHandleSearchWrapper.tsx Просмотреть файл

@@ -0,0 +1,21 @@
import React from "react";
import QrCodeHandleSearch from "./qrCodeHandleSearch";
import QrCodeHandleSearchLoading from "./qrCodeHandleSearchLoading";
import { fetchUser } from "@/app/api/user";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

interface SubComponents {
Loading: typeof QrCodeHandleSearchLoading;
}

const QrCodeHandleSearchWrapper: React.FC & SubComponents = async () => {
const [users, printerCombo] = await Promise.all([
fetchUser(),
fetchPrinterCombo(),
]);
return <QrCodeHandleSearch users={users} printerCombo={printerCombo} />;
};

QrCodeHandleSearchWrapper.Loading = QrCodeHandleSearchLoading;

export default QrCodeHandleSearchWrapper;

+ 66
- 0
src/components/qrCodeHandles/qrCodeHandleTabs.tsx Просмотреть файл

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

import { useState, ReactNode } from "react";
import { Box, Tabs, Tab } from "@mui/material";
import { useTranslation } from "react-i18next";

interface TabPanelProps {
children?: ReactNode;
index: number;
value: number;
}

function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;

return (
<div
role="tabpanel"
hidden={value !== index}
id={`qr-code-handle-tabpanel-${index}`}
aria-labelledby={`qr-code-handle-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
}

interface QrCodeHandleTabsProps {
userTabContent: ReactNode;
equipmentTabContent: ReactNode;
}

const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
userTabContent,
equipmentTabContent,
}) => {
const { t } = useTranslation("common");
const { t: tUser } = useTranslation("user");
const [currentTab, setCurrentTab] = useState(0);

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue);
};

return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={tUser("User")} />
<Tab label={t("Equipment")} />
</Tabs>
</Box>

<TabPanel value={currentTab} index={0}>
{userTabContent}
</TabPanel>

<TabPanel value={currentTab} index={1}>
{equipmentTabContent}
</TabPanel>
</Box>
);
};

export default QrCodeHandleTabs;

+ 29
- 0
src/i18n/zh/common.json Просмотреть файл

@@ -101,6 +101,23 @@
"QC Check Template": "QC檢查模板",
"Mail": "郵件",
"Import Testing": "匯入測試",
"FG":"成品",
"Qty":"數量",
"FG & Material Demand Forecast Detail":"成品及材料需求預測詳情",
"View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌",
"Delivery Order":"送貨訂單",
"Detail Scheduling":"詳細排程",
"Customer":"客戶",
"qcItem":"品檢項目",
"Item":"物料",
"Production Date":"生產日期",
"QC Check Item":"QC品檢項目",
"QC Category":"QC品檢模板",
"qcCategory":"品檢模板",
"QC Check Template":"QC檢查模板",
"QR Code Handle":"二維碼列印及下載",
"Mail":"郵件",
"Import Testing":"匯入測試",
"Overview": "總覽",
"Projects": "專案",
"Create Project": "新增專案",
@@ -112,12 +129,24 @@
"scheduling": "排程",
"settings": "設定",
"items": "物料",
"edit":"編輯",
"Edit Equipment Type":"設備類型詳情",
"Edit Equipment":"設備詳情",
"equipmentType":"設備種類",
"Description":"描述",
"edit": "編輯",
"Edit Equipment Type": "設備類型詳情",
"Edit Equipment": "設備詳情",
"equipmentType": "設備類型",
"Description": "描述",
"Details": "詳情",
"Equipment Type Details":"設備類型詳情",
"Equipment Type":"設備類型",
"Save":"儲存",
"Cancel":"取消",
"Equipment Details":"設備詳情",
"Exclude Date":"排除日期",
"Finished Goods Name":"成品名稱",
"Equipment Type Details": "設備類型詳情",
"Save": "儲存",
"Cancel": "取消",


+ 1
- 0
src/i18n/zh/jo.json Просмотреть файл

@@ -461,6 +461,7 @@
"QC Category":"QC品檢模板",
"qcCategory":"品檢模板",
"QC Check Template":"QC檢查模板",
"QR Code Handle":"二維碼列印及下載",
"Mail":"郵件",
"Import Testing":"匯入測試",
"Overview": "總覽",


+ 5
- 1
src/i18n/zh/user.json Просмотреть файл

@@ -1,5 +1,6 @@
{
"Create User": "新增用戶",
"Edit User": "編輯用戶資料",
"User Detail": "用戶詳細資料",
"User Authority": "用戶權限",
"Authority Pool": "權限池",
@@ -24,5 +25,8 @@
"Search by Authority or description or position.": "搜尋權限、描述或職位。",
"Remove": "移除",
"User": "用戶",
"user": "用戶"
"user": "用戶",
"qrcode": "二維碼",
"staffNo": "員工編號",
"Rows per page": "每頁行數"
}

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/user/req/UpdateUserReq.java Просмотреть файл

@@ -0,0 +1,3 @@




+ 3
- 0
src/main/java/com/ffii/fpsms/modules/user/web/UserController.java Просмотреть файл

@@ -0,0 +1,3 @@




Загрузка…
Отмена
Сохранить