Quellcode durchsuchen

Update customer

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui vor 1 Jahr
Ursprung
Commit
c900c8e44a
19 geänderte Dateien mit 499 neuen und 243 gelöschten Zeilen
  1. +4
    -4
      src/app/(main)/settings/customer/create/page.tsx
  2. +27
    -0
      src/app/(main)/settings/customer/edit/page.tsx
  3. +35
    -6
      src/app/api/customer/actions.ts
  4. +6
    -2
      src/app/api/customer/index.ts
  5. +2
    -2
      src/app/utils/commonUtil.ts
  6. +17
    -0
      src/app/utils/fetchUtil.ts
  7. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  8. +0
    -152
      src/components/CreateCustomer/CustomerDetails.tsx
  9. +0
    -1
      src/components/CreateCustomer/index.ts
  10. +27
    -12
      src/components/CustomerDetail/ContactInfo.tsx
  11. +102
    -30
      src/components/CustomerDetail/CustomerDetail.tsx
  12. +13
    -4
      src/components/CustomerDetail/CustomerDetailWrapper.tsx
  13. +177
    -0
      src/components/CustomerDetail/CustomerInfo.tsx
  14. +22
    -20
      src/components/CustomerDetail/SubsidiaryAllocation.tsx
  15. +1
    -0
      src/components/CustomerDetail/index.ts
  16. +26
    -1
      src/components/CustomerSearch/CustomerSearch.tsx
  17. +21
    -7
      src/components/Swal/CustomAlerts.js
  18. +9
    -1
      src/i18n/en/customer.json
  19. +9
    -1
      src/i18n/zh/customer.json

+ 4
- 4
src/app/(main)/settings/customer/create/page.tsx Datei anzeigen

@@ -1,5 +1,5 @@
import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer";
import CreateCustomer from "@/components/CreateCustomer";
import CustomerDetail from "@/components/CustomerDetail";
// import { preloadAllTasks } from "@/app/api/tasks";
import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { I18nProvider, getServerI18n } from "@/i18n";
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
title: "Create Customer",
};

const Projects: React.FC = async () => {
const CreateCustomer: React.FC = async () => {
const { t } = await getServerI18n("customer");
// fetchSubsidiaries();

@@ -18,10 +18,10 @@ const Projects: React.FC = async () => {
<>
<Typography variant="h4">{t("Create Customer")}</Typography>
<I18nProvider namespaces={["customer", "common"]}>
<CreateCustomer />
<CustomerDetail />
</I18nProvider>
</>
);
};

export default Projects;
export default CreateCustomer;

+ 27
- 0
src/app/(main)/settings/customer/edit/page.tsx Datei anzeigen

@@ -0,0 +1,27 @@
import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer";
import CustomerDetail from "@/components/CustomerDetail";
// import { preloadAllTasks } from "@/app/api/tasks";
import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";

export const metadata: Metadata = {
title: "Edit Customer",
};

const EditCustomer: React.FC = async () => {
const { t } = await getServerI18n("customer");
// fetchSubsidiaries();

return (
<>
<Typography variant="h4">{t("Edit Customer")}</Typography>
<I18nProvider namespaces={["customer", "common"]}>
<CustomerDetail />
</I18nProvider>
</>
);
};

export default EditCustomer;

+ 35
- 6
src/app/api/customer/actions.ts Datei anzeigen

@@ -1,9 +1,11 @@
import { Subsidiary } from '@/app/api/customer';
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Contact, NewCustomerResponse } from ".";
import { Contact, Customer, SaveCustomerResponse } from ".";
import { revalidateTag } from "next/cache";
import { cache } from "react";


export interface CustomerFormInputs {
@@ -14,9 +16,6 @@ export interface CustomerFormInputs {
code: string;
address: string | null;
district: string | null;
email: string | null;
phone: string | null;
contactName: string | null;
brNo: string | null;
typeId: number;

@@ -32,8 +31,14 @@ export interface CustomerFormInputs {
isGridEditing: boolean | null;
}

export interface CustomerResponse {
customer: Customer;
subsidiaryIds: number[];
contacts: Contact[];
}

export const saveCustomer = async (data: CustomerFormInputs) => {
const saveCustomer = await serverFetchJson<NewCustomerResponse>(
const saveCustomer = await serverFetchJson<SaveCustomerResponse>(
`${BASE_API_URL}/customer/save`,
{
method: "POST",
@@ -46,3 +51,27 @@ export const saveCustomer = async (data: CustomerFormInputs) => {

return saveCustomer;
};

export const fetchCustomer = async (id: number) => {
const customer = await serverFetchJson<CustomerResponse>(
`${BASE_API_URL}/customer/${id}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);

return customer
};

export const deleteCustomer = async (id: number) => {
const customer = await serverFetchWithNoContent(
`${BASE_API_URL}/customer/${id}`,
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
},
);

return customer
};

+ 6
- 2
src/app/api/customer/index.ts Datei anzeigen

@@ -7,9 +7,13 @@ export interface Customer {
id: number;
code: string;
name: string;
brNo: string | null;
address: string | null;
district: string | null;
customerType: CustomerType
}

export interface NewCustomerResponse {
export interface SaveCustomerResponse {
customer: Customer;
message: string;
}
@@ -61,7 +65,7 @@ export const fetchCustomerTypes = cache(async () => {
return serverFetchJson<CustomerType[]>(
`${BASE_API_URL}/customer/types`,
{
next: { tags: ["CustomerTypes"] },
next: { tags: ["customerTypes"] },
},
);
});

+ 2
- 2
src/app/utils/commonUtil.ts Datei anzeigen

@@ -1,7 +1,7 @@
export function getDeletedRecordWithRefList(referenceList: Array<Number>, updatedList: Array<Number>) {
export function getDeletedRecordWithRefList(referenceList: Array<number>, updatedList: Array<number>) {
return referenceList.filter(x => !updatedList.includes(x));
}

export function getNewRecordWithRefList(referenceList: Array<Number>, updatedList: Array<Number>) {
export function getNewRecordWithRefList(referenceList: Array<number>, updatedList: Array<number>) {
return updatedList.filter(x => !referenceList.includes(x));
}

+ 17
- 0
src/app/utils/fetchUtil.ts Datei anzeigen

@@ -25,6 +25,7 @@ type FetchParams = Parameters<typeof fetch>;

export async function serverFetchJson<T>(...args: FetchParams) {
const response = await serverFetch(...args);

if (response.ok) {
return response.json() as T;
} else {
@@ -38,6 +39,22 @@ export async function serverFetchJson<T>(...args: FetchParams) {
}
}

export async function serverFetchWithNoContent(...args: FetchParams) {
const response = await serverFetch(...args);

if (response.ok) {
return response.status; // 204 No Content, e.g. for delete data
} else {
switch (response.status) {
case 401:
signOutUser();
default:
console.error(await response.text());
throw Error("Something went wrong fetching data in server.");
}
}
}

export const signOutUser = () => {
const headersList = headers();
const referer = headersList.get("referer");


+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Datei anzeigen

@@ -16,6 +16,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/tasks/create": "Create Task Template",
"/settings/customer": "Customer",
"/settings/customer/create": "Create Customer",
"/settings/customer/edit": "Edit Customer",
"/settings": "Settings",
"/company": "Company",
"/settings/department": "Department",


+ 0
- 152
src/components/CreateCustomer/CustomerDetails.tsx Datei anzeigen

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

import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import { Controller, useFormContext } from "react-hook-form";
import { CustomerFormInputs } from "@/app/api/customer/actions";
import { CustomerType } from "@/app/api/customer";
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import ContactDetails from "./ContactDetails";

interface Props {
customerTypes: CustomerType[],
}

const CustomerDetails: React.FC<Props> = ({
customerTypes,
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
control,
reset
} = useFormContext<CustomerFormInputs>();

return (
<>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Customer Details")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Customer Code")}
fullWidth
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={Boolean(errors.code) && (errors.code?.message ? t(errors.code.message) : t("Please input correct customer code"))}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Name")}
fullWidth
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={Boolean(errors.name) && t("Please input correct customer name")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Address")}
fullWidth
{...register("address")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer District")}
fullWidth
{...register("district")}
/>
</Grid>
{/* <Grid item xs={6}>
<TextField
label={t("Customer Email")}
fullWidth
{...register("email", {
pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/,
})}
error={Boolean(errors.email)}
helperText={Boolean(errors.email) && t("Please input correct customer email")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Phone")}
fullWidth
{...register("phone")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Contact Name")}
fullWidth
{...register("contactName")}
/>
</Grid> */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Customer Type")}</InputLabel>
<Controller
defaultValue={customerTypes[0].id}
control={control}
name="typeId"
render={({ field }) => (
<Select label={t("Project Category")} {...field}>
{customerTypes.map((type, index) => (
<MenuItem
key={`${type.id}-${index}`}
value={type.id}
>
{t(type.name)}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Br No.")}
fullWidth
{...register("brNo", {
pattern: /[0-9]{8}/,
})}
error={Boolean(errors.brNo)}
helperText={Boolean(errors.brNo) && t("Please input correct customer br no.")}
/>
</Grid>
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button onClick={() => reset()} variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions>
</CardContent>
</Card>
<ContactDetails/>
</>
);
};

export default CustomerDetails;

+ 0
- 1
src/components/CreateCustomer/index.ts Datei anzeigen

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

src/components/CreateCustomer/ContactDetails.tsx → src/components/CustomerDetail/ContactInfo.tsx Datei anzeigen

@@ -1,10 +1,8 @@
"use client";

import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
@@ -19,7 +17,6 @@ import {
GridRowsProp,
GridRowModesModel,
GridRowModes,
DataGrid,
GridColDef,
GridToolbarContainer,
GridActionsCellItem,
@@ -27,11 +24,8 @@ import {
GridRowId,
GridRowModel,
GridRowEditStopReasons,
GridPreProcessEditCellProps,
GridCellParams,
} from '@mui/x-data-grid';
import CustomDatagrid from "../CustomDatagrid/CustomDatagrid";
import { Contact } from "@/app/api/customer";
import { useFieldArray, useFormContext } from "react-hook-form";
import { useCallback, useEffect, useMemo, useState } from "react";

@@ -69,11 +63,11 @@ function EditToolbar(props: EditToolbarProps) {
);
}

const ContactDetails: React.FC<Props> = ({
const ContactInfo: React.FC<Props> = ({
}) => {
const { t } = useTranslation();

const { control, setValue, getValues, formState: { errors }, setError, clearErrors } = useFormContext();
const { control, setValue, getValues, formState: { errors, defaultValues }, setError, clearErrors, reset, watch, resetField } = useFormContext();
const { fields } = useFieldArray({
control,
name: "addContacts"
@@ -89,9 +83,16 @@ const ContactDetails: React.FC<Props> = ({
})
})

const [rows, setRows] = useState(initialRows);
const [rows, setRows] = useState<GridRowsProp>([]);
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});

useEffect(() => {
if (initialRows.length > 0 && rows.length === 0) {
console.log("first")
setRows(initialRows)
}
}, [initialRows.length > 0])

const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
@@ -137,6 +138,15 @@ const ContactDetails: React.FC<Props> = ({
setRowModesModel(newRowModesModel);
}, [rows]);

const resetContact = useCallback(() => {
if (defaultValues !== undefined) {
resetField("addContacts")
// reset({addContacts: defaultValues.addContacts})
setRows((prev) => defaultValues.addContacts)
setRowModesModel(rows.reduce((acc, row) => ({...acc, [row.id]: { mode: GridRowModes.View } }), {}))
}
}, [defaultValues])

const columns = useMemo<GridColDef[]>(
() => [
{
@@ -210,6 +220,10 @@ const ContactDetails: React.FC<Props> = ({

// check error
useEffect(() => {
if (getValues("addContacts") !== undefined || getValues("addContacts") !== null) {
return;
}

if (getValues("addContacts").length === 0) {
clearErrors("addContacts")
} else {
@@ -240,13 +254,14 @@ const ContactDetails: React.FC<Props> = ({
setValue("isGridEditing", false)
}
}, [rowModesModel])

return (
<Card sx={{ display: "block" }}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Stack gap={2}>
{/* <div> */}
<Typography variant="overline" display='inline-block' noWrap>
{t("Contact Details")}
{t("Contact Info")}
</Typography>
{Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap>
{t("Please ensure all the fields are inputted and saved")}
@@ -275,7 +290,7 @@ const ContactDetails: React.FC<Props> = ({
}}
/>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
<Button variant="text" startIcon={<RestartAlt />} onClick={resetContact} disabled={Boolean(watch("isGridEditing"))}>
{t("Reset")}
</Button>
</CardActions>
@@ -285,4 +300,4 @@ const ContactDetails: React.FC<Props> = ({
);
};

export default ContactDetails;
export default ContactInfo;

src/components/CreateCustomer/CreateCustomer.tsx → src/components/CustomerDetail/CustomerDetail.tsx Datei anzeigen

@@ -6,8 +6,8 @@ import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
FieldErrors,
@@ -18,8 +18,8 @@ import {
} from "react-hook-form";
import { Error } from "@mui/icons-material";
import { Typography } from "@mui/material";
import { CustomerFormInputs, saveCustomer } from "@/app/api/customer/actions";
import CustomerDetails from "./CustomerDetails";
import { CustomerFormInputs, fetchCustomer, saveCustomer } from "@/app/api/customer/actions";
import CustomerInfo from "./CustomerInfo";
import SubsidiaryAllocation from "./SubsidiaryAllocation";
import { CustomerType, Subsidiary } from "@/app/api/customer";
import { getDeletedRecordWithRefList } from "@/app/utils/commonUtil";
@@ -42,25 +42,97 @@ const hasErrorsInTab = (
}
};

const CreateCustomer: React.FC<Props> = ({
const CustomerDetail: React.FC<Props> = ({
subsidiaries,
customerTypes,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const [refCustomer, setRefCustomer] = useState<CustomerFormInputs>()
const { t } = useTranslation();
const router = useRouter();
const searchParams = useSearchParams()

const fetchCurrentCustomer = async () => {
const id = searchParams.get('id')
try {
const defaultCustomer = {
id: null,
code: "",
name: "",
brNo: null,
address: null,
district: null,
typeId: 1,
addContacts: [],
addSubsidiaryIds: [],
deleteSubsidiaryIds: [],
deleteContactIds: [],
isGridEditing: false
}

if (id !== null && parseInt(id) > 0) {

const customer = await fetchCustomer(parseInt(id))

if (customer !== null && Object.keys(customer).length > 0) {
const tempCustomerInput = {
id: customer.customer.id,
code: customer.customer.code ?? "",
name: customer.customer.name ?? "",
brNo: customer.customer.brNo ?? "",
address: customer.customer.address ?? "",
district: customer.customer.district ?? "",
typeId: customer.customer.customerType.id,
addContacts: customer.contacts ?? [],
addSubsidiaryIds: customer.subsidiaryIds ?? [],
deleteSubsidiaryIds: [],
deleteContactIds: [],
isGridEditing: false
}
setRefCustomer(tempCustomerInput)
} else {
setRefCustomer(defaultCustomer)
}
} else {
setRefCustomer(defaultCustomer)
}
} catch (e) {
console.log(e)
setServerError(t("An error has occurred. Please try again later."));
}
}

useLayoutEffect(() => {
fetchCurrentCustomer()
}, [])

const formProps = useForm<CustomerFormInputs>({
defaultValues: {
code: "",
name: "",
addContacts: [],
addSubsidiaryIds: [],
deleteSubsidiaryIds: [],
deleteContactIds: []
},
// defaultValues: useMemo(() => {
// return refCustomer;
// }, [refCustomer])
// defaultValues: {
// id: null,
// code: "",
// name: "",
// brNo: null,
// address: null,
// district: null,
// typeId: 1,
// addContacts: [],
// addSubsidiaryIds: [],
// deleteSubsidiaryIds: [],
// deleteContactIds: [],
// isGridEditing: false
// }
});

useEffect(() => {
if (refCustomer !== null && refCustomer !== undefined) {
formProps.reset(refCustomer)
}
}, [refCustomer])

const handleCancel = () => {
router.back();
};
@@ -79,7 +151,7 @@ const CreateCustomer: React.FC<Props> = ({
warningDialog(t("Please save all the rows before submitting"), t)
return false
}
console.log(data);

let haveError = false
@@ -93,10 +165,10 @@ const CreateCustomer: React.FC<Props> = ({
formProps.setError("code", { message: "Code is empty", type: "required" })
}

if (data.email && data.email?.length > 0 && !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(data.email)) {
haveError = true
formProps.setError("email", { message: "Email format is not valid", type: "custom" })
}
// if (data.email && data.email?.length > 0 && !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(data.email)) {
// haveError = true
// formProps.setError("email", { message: "Email format is not valid", type: "custom" })
// }

if (data.brNo && data.brNo?.length > 0 && !/[0-9]{8}/.test(data.brNo)) {
haveError = true
@@ -105,12 +177,12 @@ const CreateCustomer: React.FC<Props> = ({

if (data.addContacts.length === 0 || data.addContacts.filter(row => String(row.name).trim().length === 0 || String(row.phone).trim().length === 0 || String(row.email).trim().length === 0).length > 0) {
haveError = true
formProps.setError("addContacts", { message: "Contact details include empty fields", type: "required" })
formProps.setError("addContacts", { message: "Contact info include empty fields", type: "required" })
}

if (data.addContacts.length > 0 && data.addContacts.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))).length > 0) {
haveError = true
formProps.setError("addContacts", { message: "Contact details include empty fields", type: "email_format" })
formProps.setError("addContacts", { message: "Contact info include empty fields", type: "email_format" })
}

if (haveError) {
@@ -119,9 +191,9 @@ const CreateCustomer: React.FC<Props> = ({
return false
}

// data.deleteSubsidiaryIds = data.deleteSubsidiaryIds ?? []
// data.addSubsidiaryIds = data.addSubsidiaryIds ?? []
// data.deleteContactIds = data.deleteContactIds ?? []
data.deleteContactIds = getDeletedRecordWithRefList(refCustomer?.addContacts.map(contact => contact.id)!!, data.addContacts.map(contact => contact.id)!!)
data.deleteSubsidiaryIds = getDeletedRecordWithRefList(refCustomer?.addSubsidiaryIds!!, data.addSubsidiaryIds)
setServerError("");

submitDialog(async () => {
@@ -144,7 +216,7 @@ const CreateCustomer: React.FC<Props> = ({
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t],
[router, t, refCustomer],
);

const onSubmitError = useCallback<SubmitErrorHandler<CustomerFormInputs>>(
@@ -161,14 +233,14 @@ const CreateCustomer: React.FC<Props> = ({

return (
<FormProvider {...formProps}>
<Stack
{refCustomer && <Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab
label={t("Customer Details")}
label={t("Customer Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
@@ -183,7 +255,7 @@ const CreateCustomer: React.FC<Props> = ({
{serverError}
</Typography>
)}
{tabIndex === 0 && <CustomerDetails customerTypes={customerTypes} />}
{tabIndex === 0 && <CustomerInfo customerTypes={customerTypes} />}
{tabIndex === 1 && <SubsidiaryAllocation subsidiaries={subsidiaries} />}

<Stack direction="row" justifyContent="flex-end" gap={1}>
@@ -194,13 +266,13 @@ const CreateCustomer: React.FC<Props> = ({
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
<Button variant="contained" startIcon={<Check />} type="submit" disabled={Boolean(formProps.watch("isGridEditing"))}>
{t("Confirm")}
</Button>
</Stack>
</Stack>
</Stack>}
</FormProvider>
);
};

export default CreateCustomer;
export default CustomerDetail;

src/components/CreateCustomer/CreateCustomerWrapper.tsx → src/components/CustomerDetail/CustomerDetailWrapper.tsx Datei anzeigen

@@ -3,9 +3,18 @@
// import { fetchProjectCategories } from "@/app/api/projects";
// import { fetchTeamLeads } from "@/app/api/staff";
import { fetchCustomerTypes, fetchSubsidiaries } from "@/app/api/customer";
import CreateCustomer from "./CreateCustomer";
import CustomerDetail from "./CustomerDetail";
import { getServerSideProps } from "next/dist/build/templates/pages";

const CreateCustomerWrapper: React.FC = async () => {
// type Props = {
// params: {
// id: string | undefined;
// };
// };

const CustomerDetailWrapper: React.FC = async () => {
// const { params } = props
// console.log(params)
const [subsidiaries, customerTypes] =
await Promise.all([
fetchSubsidiaries(),
@@ -13,8 +22,8 @@ const CreateCustomerWrapper: React.FC = async () => {
]);

return (
<CreateCustomer subsidiaries={subsidiaries} customerTypes={customerTypes}/>
<CustomerDetail subsidiaries={subsidiaries} customerTypes={customerTypes} />
);
};

export default CreateCustomerWrapper;
export default CustomerDetailWrapper;

+ 177
- 0
src/components/CustomerDetail/CustomerInfo.tsx Datei anzeigen

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

import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import { Controller, useFormContext } from "react-hook-form";
import { CustomerFormInputs } from "@/app/api/customer/actions";
import { CustomerType } from "@/app/api/customer";
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import ContactInfo from "./ContactInfo";
import { useCallback } from "react";

interface Props {
customerTypes: CustomerType[],
}

const CustomerInfo: React.FC<Props> = ({
customerTypes,
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors, defaultValues },
control,
reset,
resetField,
setValue
} = useFormContext<CustomerFormInputs>();

const resetCustomer = useCallback(() => {
console.log(defaultValues)
if (defaultValues !== undefined) {
resetField("code")
resetField("name")
resetField("address")
resetField("district")
resetField("typeId")
resetField("brNo")

// setValue("code", defaultValues.code ?? "")
// reset({
// code: defaultValues.code,
// name: defaultValues.name,
// address: defaultValues.address,
// district: defaultValues.district,
// typeId: defaultValues.typeId,
// brNo: defaultValues.brNo
// })
}
}, [defaultValues])

return (
<>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Customer Info")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Customer Code")}
fullWidth
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={Boolean(errors.code) && (errors.code?.message ? t(errors.code.message) : t("Please input correct customer code"))}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Name")}
fullWidth
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={Boolean(errors.name) && t("Please input correct customer name")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Address")}
fullWidth
{...register("address")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer District")}
fullWidth
{...register("district")}
/>
</Grid>
{/* <Grid item xs={6}>
<TextField
label={t("Customer Email")}
fullWidth
{...register("email", {
pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/,
})}
error={Boolean(errors.email)}
helperText={Boolean(errors.email) && t("Please input correct customer email")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Phone")}
fullWidth
{...register("phone")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Contact Name")}
fullWidth
{...register("contactName")}
/>
</Grid> */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Customer Type")}</InputLabel>
<Controller
defaultValue={customerTypes[0].id}
control={control}
name="typeId"
render={({ field }) => (
<Select label={t("Project Category")} {...field}>
{customerTypes.map((type, index) => (
<MenuItem
key={`${type.id}-${index}`}
value={type.id}
>
{t(type.name)}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Br No.")}
fullWidth
{...register("brNo", {
pattern: /[0-9]{8}/,
})}
error={Boolean(errors.brNo)}
helperText={Boolean(errors.brNo) && t("Please input correct customer br no.")}
/>
</Grid>
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button onClick={resetCustomer} variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions>
</CardContent>
</Card>
<ContactInfo />
</>
);
};

export default CustomerInfo;

src/components/CreateCustomer/SubsidiaryAllocation.tsx → src/components/CustomerDetail/SubsidiaryAllocation.tsx Datei anzeigen

@@ -12,10 +12,6 @@ import {
TextField,
InputAdornment,
IconButton,
FormControl,
InputLabel,
Select,
MenuItem,
Box,
Button,
Card,
@@ -24,12 +20,9 @@ import {
TabsProps,
Tab,
Tabs,
SelectChangeEvent,
} from "@mui/material";
import differenceBy from "lodash/differenceBy";
import uniq from "lodash/uniq";
import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { CustomerFormInputs } from "@/app/api/customer/actions";
import { Subsidiary } from "@/app/api/customer";

@@ -41,7 +34,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({
subsidiaries,
}) => {
const { t } = useTranslation();
const { setValue, getValues } = useFormContext<CustomerFormInputs>();
const { setValue, getValues, formState: { defaultValues }, reset, resetField } = useFormContext<CustomerFormInputs>();

const [filteredSubsidiary, setFilteredSubsidiary] = React.useState(subsidiaries);
const [selectedSubsidiary, setSelectedSubsidiary] = React.useState<
@@ -49,19 +42,28 @@ const SubsidiaryAllocation: React.FC<Props> = ({
>(
subsidiaries.filter((subsidiary) =>
getValues("addSubsidiaryIds")?.includes(subsidiary.id),
),
)
);

// Adding / Removing staff
const addSubsidiary = React.useCallback((subsidiary: Subsidiary) => {
setSelectedSubsidiary((subsidiaries) => [...subsidiaries, subsidiary]);
}, []);

const removeSubsidiary = React.useCallback((subsidiary: Subsidiary) => {
setSelectedSubsidiary((subsidiaries) => subsidiaries.filter((s) => s.id !== subsidiary.id));
}, []);

const clearSubsidiary = React.useCallback(() => {
setSelectedSubsidiary([]);
}, []);
if (defaultValues !== undefined) {
// reset({ addSubsidiaryIds: defaultValues.addSubsidiaryIds })
resetField("addSubsidiaryIds")
setSelectedSubsidiary(subsidiaries.filter((subsidiary) =>
defaultValues.addSubsidiaryIds?.includes(subsidiary.id),
))
}
}, [defaultValues]);

// Sync with form
useEffect(() => {
setValue(
@@ -81,11 +83,11 @@ const SubsidiaryAllocation: React.FC<Props> = ({
{ label: t("Subsidiary Code"), name: "code" },
{ label: t("Subsidiary Name"), name: "name" },
{ label: t("Subsidiary Br No."), name: "brNo" },
{ label: t("Subsidiary Contact Name"), name: "contactName" },
{ label: t("Subsidiary Phone"), name: "phone" },
// { label: t("Subsidiary Contact Name"), name: "contactName" },
// { label: t("Subsidiary Phone"), name: "phone" },
{ label: t("Subsidiary Address"), name: "address" },
{ label: t("Subsidiary District"), name: "district" },
{ label: t("Subsidiary Email"), name: "email" },
// { label: t("Subsidiary Email"), name: "email" },
],
[addSubsidiary, t],
);
@@ -101,11 +103,11 @@ const SubsidiaryAllocation: React.FC<Props> = ({
{ label: t("Subsidiary Code"), name: "code" },
{ label: t("Subsidiary Name"), name: "name" },
{ label: t("Subsidiary Br No."), name: "brNo" },
{ label: t("Subsidiary Contact Name"), name: "contactName" },
{ label: t("Subsidiary Phone"), name: "phone" },
// { label: t("Subsidiary Contact Name"), name: "contactName" },
// { label: t("Subsidiary Phone"), name: "phone" },
{ label: t("Subsidiary Address"), name: "address" },
{ label: t("Subsidiary District"), name: "district" },
{ label: t("Subsidiary Email"), name: "email" },
// { label: t("Subsidiary Email"), name: "email" },
],
[removeSubsidiary, t],
);
@@ -143,14 +145,14 @@ const SubsidiaryAllocation: React.FC<Props> = ({
[],
);

const reset = React.useCallback(() => {
const resetSubsidiary = React.useCallback(() => {
clearQueryInput();
clearSubsidiary();
}, [clearQueryInput, clearSubsidiary]);

return (
<>
<Card sx={{ display: "block"}}>
<Card sx={{ display: "block" }}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Stack gap={2}>
<Typography variant="overline" display="block">
@@ -201,7 +203,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({
</Box>
</Stack>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />} onClick={reset}>
<Button variant="text" startIcon={<RestartAlt />} onClick={resetSubsidiary}>
{t("Reset")}
</Button>
</CardActions>

+ 1
- 0
src/components/CustomerDetail/index.ts Datei anzeigen

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

+ 26
- 1
src/components/CustomerSearch/CustomerSearch.tsx Datei anzeigen

@@ -6,6 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import DeleteIcon from '@mui/icons-material/Delete';
import { useRouter, useSearchParams } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { deleteCustomer } from "@/app/api/customer/actions";

interface Props {
customers: Customer[];
@@ -16,6 +20,8 @@ type SearchParamNames = keyof SearchQuery;

const CustomerSearch: React.FC<Props> = ({ customers }) => {
const { t } = useTranslation();
const router = useRouter()
const searchParams = useSearchParams()

const [filteredCustomers, setFilteredCustomers] = useState(customers);
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -30,7 +36,20 @@ const CustomerSearch: React.FC<Props> = ({ customers }) => {
}, [customers]);

const onTaskClick = useCallback((customer: Customer) => {
console.log(customer);
const params = new URLSearchParams(searchParams.toString())
params.set("id", customer.id.toString())
router.replace(`/settings/customer/edit?${params.toString()}`);
}, []);

const onDeleteClick = useCallback((customer: Customer) => {

deleteDialog(async() => {
await deleteCustomer(customer.id)

successDialog("Delete Success", t)

setFilteredCustomers((prev) => prev.filter((obj) => obj.id !== customer.id))
}, t)
}, []);

const columns = useMemo<Column<Customer>[]>(
@@ -43,6 +62,12 @@ const CustomerSearch: React.FC<Props> = ({ customers }) => {
},
{ name: "code", label: t("Customer Code") },
{ name: "name", label: t("Customer Name") },
{
name: "id",
label: t("Delete"),
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
},
],
[onTaskClick, t],
);


+ 21
- 7
src/components/Swal/CustomAlerts.js Datei anzeigen

@@ -50,18 +50,32 @@ export const warningDialog = (text, t) => {
})
}

export const submitDialog = (confirmAction, t) => {
export const submitDialog = async (confirmAction, t) => {
// const { t } = useTranslation("common")
return Swal.fire({
const result = await Swal.fire({
icon: "question",
title: t("Do you want to submit?"),
cancelButtonText: t("Cancel"),
confirmButtonText: t("Submit"),
showCancelButton: true,
showConfirmButton: true,
}).then((result) => {
if (result.isConfirmed) {
confirmAction()
}
})
});
if (result.isConfirmed) {
confirmAction();
}
}

export const deleteDialog = async (confirmAction, t) => {
// const { t } = useTranslation("common")
const result = await Swal.fire({
icon: "question",
title: t("Do you want to delete?"),
cancelButtonText: t("Cancel"),
confirmButtonText: t("Delete"),
showCancelButton: true,
showConfirmButton: true,
});
if (result.isConfirmed) {
confirmAction();
}
}

+ 9
- 1
src/i18n/en/customer.json Datei anzeigen

@@ -11,6 +11,7 @@
"Customer Contact Name": "Client Contact Name",
"Customer Br No.": "Client Br No.",
"Customer Details": "Client Details",
"Customer Info": "Client Info",
"Customer Type": "Client Type",

"Please input correct customer code": "Please input correct client code",
@@ -33,9 +34,11 @@
"Subsidiary Contact Name": "Subsidiary Contact Name",
"Subsidiary Br No.": "Subsidiary Br No.",
"Subsidiary Details": "Subsidiary Details",
"Subsidiary Info": "Subsidiary Info",

"Add Contact Person": "Add Contact Person",
"Contact Details": "Contact Details",
"Contact Info": "Contact Info",
"Contact Name": "Contact Name",
"Contact Email": "Contact Email",
"Contact Phone": "Contact Phone",
@@ -44,13 +47,18 @@

"Do you want to submit?": "Do you want to submit?",
"Submit Success": "Submit Success",
"Submit Fail": "Submit Fail",
"Do you want to delete?": "Do you want to delete",
"Delete Success": "Delete Success",

"Add": "Add",
"Details": "Details",
"Info": "Info",
"Search": "Search",
"Search Criteria": "Search Criteria",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Submit": "Submit",
"Reset": "Reset"
"Reset": "Reset",
"Delete": "Delete"
}

+ 9
- 1
src/i18n/zh/customer.json Datei anzeigen

@@ -11,6 +11,7 @@
"Customer Contact Name": "客戶聯絡名稱",
"Customer Br No.": "客戶商業登記號碼",
"Customer Details": "客戶詳請",
"Customer Info": "客戶資料",
"Customer Type": "客戶類型",

"Please input correct customer code": "請輸入客戶編號",
@@ -33,9 +34,11 @@
"Subsidiary Contact Name": "子公司聯絡名稱",
"Subsidiary Br No.": "子公司商業登記號碼",
"Subsidiary Details": "子公司詳請",
"Subsidiary Info": "子公司資料",

"Add Contact Person": "新增聯絡人",
"Contact Details": "聯絡詳請",
"Contact Info": "聯絡資料",
"Contact Name": "聯絡姓名",
"Contact Email": "聯絡電郵",
"Contact Phone": "聯絡電話",
@@ -44,13 +47,18 @@
"Do you want to submit?": "你是否確認要提交?",
"Submit Success": "提交成功",
"Submit Fail": "提交失敗",
"Do you want to delete?": "你是否確認要刪除?",
"Delete Success": "刪除成功",

"Add": "新增",
"Details": "詳請",
"Info": "資料",
"Search": "搜尋",
"Search Criteria": "搜尋條件",
"Cancel": "取消",
"Confirm": "確認",
"Submit": "提交",
"Reset": "重置"
"Reset": "重置",
"Delete": "刪除"
}

Laden…
Abbrechen
Speichern