@@ -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; |
@@ -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; |
@@ -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 | |||
}; |
@@ -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"] }, | |||
}, | |||
); | |||
}); |
@@ -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)); | |||
} |
@@ -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"); | |||
@@ -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", | |||
@@ -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; |
@@ -1 +0,0 @@ | |||
export { default } from "./CreateCustomerWrapper"; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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> |
@@ -0,0 +1 @@ | |||
export { default } from "./CustomerDetailWrapper"; |
@@ -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], | |||
); | |||
@@ -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(); | |||
} | |||
} |
@@ -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" | |||
} |
@@ -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": "刪除" | |||
} |