| @@ -17,7 +17,7 @@ const Projects: React.FC = async () => { | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Typography variant="h4">{t("Create Customer")}</Typography> | <Typography variant="h4">{t("Create Customer")}</Typography> | ||||
| <I18nProvider namespaces={["customer"]}> | |||||
| <I18nProvider namespaces={["customer", "common"]}> | |||||
| <CreateCustomer /> | <CreateCustomer /> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| @@ -33,7 +33,7 @@ const Customer: React.FC = async () => { | |||||
| variant="contained" | variant="contained" | ||||
| startIcon={<Add />} | startIcon={<Add />} | ||||
| LinkComponent={Link} | LinkComponent={Link} | ||||
| href="/customer/create" | |||||
| href="/settings/customer/create" | |||||
| > | > | ||||
| {t("Create Customer")} | {t("Create Customer")} | ||||
| </Button> | </Button> | ||||
| @@ -2,12 +2,14 @@ | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { Customer } from "."; | |||||
| import { Contact, NewCustomerResponse } from "."; | |||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| export interface CustomerFormInputs { | export interface CustomerFormInputs { | ||||
| // Customer details | // Customer details | ||||
| id: number | null; | |||||
| name: string; | name: string; | ||||
| code: string; | code: string; | ||||
| address: string | null; | address: string | null; | ||||
| @@ -16,14 +18,22 @@ export interface CustomerFormInputs { | |||||
| phone: string | null; | phone: string | null; | ||||
| contactName: string | null; | contactName: string | null; | ||||
| brNo: string | null; | brNo: string | null; | ||||
| typeId: number; | |||||
| // Subsidiary | // Subsidiary | ||||
| addSubsidiaryIds: number[]; | addSubsidiaryIds: number[]; | ||||
| deleteSubsidiaryIds: number[]; | deleteSubsidiaryIds: number[]; | ||||
| // Contact | |||||
| addContacts: Contact[]; | |||||
| deleteContactIds: number[]; | |||||
| // is grid editing | |||||
| isGridEditing: boolean | null; | |||||
| } | } | ||||
| export const saveCustomer = async (data: CustomerFormInputs) => { | export const saveCustomer = async (data: CustomerFormInputs) => { | ||||
| const saveCustomer = await serverFetchJson<Customer>( | |||||
| const saveCustomer = await serverFetchJson<NewCustomerResponse>( | |||||
| `${BASE_API_URL}/customer/save`, | `${BASE_API_URL}/customer/save`, | ||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| @@ -9,6 +9,16 @@ export interface Customer { | |||||
| name: string; | name: string; | ||||
| } | } | ||||
| export interface NewCustomerResponse { | |||||
| customer: Customer; | |||||
| message: string; | |||||
| } | |||||
| export interface CustomerType { | |||||
| id: number; | |||||
| name: string; | |||||
| } | |||||
| export interface Subsidiary { | export interface Subsidiary { | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| @@ -22,6 +32,14 @@ export interface Subsidiary { | |||||
| email: string | null; | email: string | null; | ||||
| } | } | ||||
| export interface Contact { | |||||
| id: number; | |||||
| name: string; | |||||
| phone: string; | |||||
| email: string; | |||||
| isNew: boolean; | |||||
| } | |||||
| export const preloadAllCustomers = () => { | export const preloadAllCustomers = () => { | ||||
| fetchAllCustomers(); | fetchAllCustomers(); | ||||
| }; | }; | ||||
| @@ -38,3 +56,12 @@ export const fetchSubsidiaries = cache(async () => { | |||||
| }, | }, | ||||
| ); | ); | ||||
| }); | }); | ||||
| export const fetchCustomerTypes = cache(async () => { | |||||
| return serverFetchJson<CustomerType[]>( | |||||
| `${BASE_API_URL}/customer/types`, | |||||
| { | |||||
| next: { tags: ["CustomerTypes"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| @@ -14,8 +14,8 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
| "/projects/create": "Create Project", | "/projects/create": "Create Project", | ||||
| "/tasks": "Task Template", | "/tasks": "Task Template", | ||||
| "/tasks/create": "Create Task Template", | "/tasks/create": "Create Task Template", | ||||
| "/customer": "Customer", | |||||
| "/customer/create": "Create Customer", | |||||
| "/settings/customer": "Customer", | |||||
| "/settings/customer/create": "Create Customer", | |||||
| "/settings": "Settings", | "/settings": "Settings", | ||||
| "/company": "Company", | "/company": "Company", | ||||
| "/settings/department": "Department", | "/settings/department": "Department", | ||||
| @@ -0,0 +1,288 @@ | |||||
| "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"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import AddIcon from '@mui/icons-material/Add'; | |||||
| import EditIcon from '@mui/icons-material/Edit'; | |||||
| import DeleteIcon from '@mui/icons-material/DeleteOutlined'; | |||||
| import SaveIcon from '@mui/icons-material/Save'; | |||||
| import CancelIcon from '@mui/icons-material/Close'; | |||||
| import { | |||||
| GridRowsProp, | |||||
| GridRowModesModel, | |||||
| GridRowModes, | |||||
| DataGrid, | |||||
| GridColDef, | |||||
| GridToolbarContainer, | |||||
| GridActionsCellItem, | |||||
| GridEventListener, | |||||
| 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"; | |||||
| interface Props { | |||||
| } | |||||
| interface EditToolbarProps { | |||||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
| setRowModesModel: ( | |||||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
| ) => void; | |||||
| } | |||||
| var rowId = -1 | |||||
| function EditToolbar(props: EditToolbarProps) { | |||||
| const { setRows, setRowModesModel } = props; | |||||
| const { t } = useTranslation(); | |||||
| const handleClick = () => { | |||||
| const id = rowId; | |||||
| rowId = rowId - 1; | |||||
| setRows((oldRows) => [{ id, name: '', phone: '', email: '', isNew: true }, ...oldRows]); | |||||
| setRowModesModel((oldModel) => ({ | |||||
| ...oldModel, | |||||
| [id]: { mode: GridRowModes.Edit, fieldToFocus: 'name' }, | |||||
| })); | |||||
| }; | |||||
| return ( | |||||
| <GridToolbarContainer> | |||||
| <Button color="primary" startIcon={<AddIcon />} onClick={handleClick}> | |||||
| {t("Add Contact Person")} | |||||
| </Button> | |||||
| </GridToolbarContainer> | |||||
| ); | |||||
| } | |||||
| const ContactDetails: React.FC<Props> = ({ | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { control, setValue, getValues, formState: { errors }, setError, clearErrors } = useFormContext(); | |||||
| const { fields } = useFieldArray({ | |||||
| control, | |||||
| name: "addContacts" | |||||
| }) | |||||
| const initialRows: GridRowsProp = fields.map((item, index) => { | |||||
| return ({ | |||||
| id: Number(getValues(`addContacts[${index}].id`)), | |||||
| name: getValues(`addContacts[${index}].name`), | |||||
| phone: getValues(`addContacts[${index}].phone`), | |||||
| email: getValues(`addContacts[${index}].email`), | |||||
| isNew: false, | |||||
| }) | |||||
| }) | |||||
| const [rows, setRows] = useState(initialRows); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
| const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { | |||||
| if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||||
| event.defaultMuiPrevented = true; | |||||
| } | |||||
| }; | |||||
| const handleEditClick = (id: GridRowId) => () => { | |||||
| setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); | |||||
| }; | |||||
| const handleSaveClick = (id: GridRowId) => () => { | |||||
| setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); | |||||
| }; | |||||
| const handleDeleteClick = (id: GridRowId) => () => { | |||||
| const updatedRows = rows.filter((row) => row.id !== id) | |||||
| setRows(updatedRows); | |||||
| setValue("addContacts", updatedRows) | |||||
| }; | |||||
| const handleCancelClick = (id: GridRowId) => () => { | |||||
| setRowModesModel({ | |||||
| ...rowModesModel, | |||||
| [id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||||
| }); | |||||
| const editedRow = rows.find((row) => row.id === id); | |||||
| if (editedRow!.isNew) { | |||||
| setRows(rows.filter((row) => row.id !== id)); | |||||
| } | |||||
| }; | |||||
| const processRowUpdate = useCallback((newRow: GridRowModel) => { | |||||
| const updatedRow = { ...newRow }; | |||||
| const updatedRows = rows.map((row) => (row.id === newRow.id ? updatedRow : row)) | |||||
| setRows(updatedRows); | |||||
| setValue("addContacts", updatedRows) | |||||
| return updatedRow; | |||||
| }, [rows]); | |||||
| const handleRowModesModelChange = useCallback((newRowModesModel: GridRowModesModel) => { | |||||
| setRowModesModel(newRowModesModel); | |||||
| }, [rows]); | |||||
| const columns = useMemo<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: 'name', | |||||
| headerName: t('Contact Name'), | |||||
| editable: true, | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: 'phone', | |||||
| headerName: t('Contact Phone'), | |||||
| editable: true, | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: 'email', | |||||
| headerName: t('Contact Email'), | |||||
| editable: true, | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: 'actions', | |||||
| type: 'actions', | |||||
| headerName: '', | |||||
| flex: 0.6, | |||||
| // width: 100, | |||||
| cellClassName: 'actions', | |||||
| getActions: ({ id, ...params }) => { | |||||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||||
| if (isInEditMode) { | |||||
| return [ | |||||
| <GridActionsCellItem | |||||
| icon={<SaveIcon />} | |||||
| label="Save" | |||||
| sx={{ | |||||
| color: 'primary.main', | |||||
| }} | |||||
| onClick={handleSaveClick(id)} | |||||
| />, | |||||
| <GridActionsCellItem | |||||
| icon={<CancelIcon />} | |||||
| label="Cancel" | |||||
| className="textPrimary" | |||||
| onClick={handleCancelClick(id)} | |||||
| color="inherit" | |||||
| />, | |||||
| ]; | |||||
| } | |||||
| return [ | |||||
| <GridActionsCellItem | |||||
| icon={<EditIcon />} | |||||
| label="Edit" | |||||
| className="textPrimary" | |||||
| onClick={handleEditClick(id)} | |||||
| color="inherit" | |||||
| />, | |||||
| <GridActionsCellItem | |||||
| icon={<DeleteIcon />} | |||||
| label="Delete" | |||||
| onClick={handleDeleteClick(id)} | |||||
| color="inherit" | |||||
| />, | |||||
| ]; | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| [rows, rowModesModel, t], | |||||
| ); | |||||
| // check error | |||||
| useEffect(() => { | |||||
| if (getValues("addContacts").length === 0) { | |||||
| clearErrors("addContacts") | |||||
| } else { | |||||
| const errorRows = rows.filter(row => String(row.name).trim().length === 0 || String(row.phone).trim().length === 0 || String(row.email).trim().length === 0) | |||||
| if (errorRows.length > 0) { | |||||
| setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | |||||
| } else { | |||||
| const errorRows_EmailFormat = rows.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))) | |||||
| if (errorRows_EmailFormat.length > 0) { | |||||
| setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | |||||
| } else { | |||||
| clearErrors("addContacts") | |||||
| } | |||||
| } | |||||
| } | |||||
| }, [rows]) | |||||
| // check editing | |||||
| useEffect(() => { | |||||
| const filteredByKey = Object.fromEntries( | |||||
| Object.entries(rowModesModel).filter(([key, value]) => rowModesModel[key].mode === 'edit')) | |||||
| if (Object.keys(filteredByKey).length > 0) { | |||||
| setValue("isGridEditing", true) | |||||
| } else { | |||||
| 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")} | |||||
| </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")} | |||||
| </Typography>} | |||||
| {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | |||||
| {t("Please ensure all the email formats are correct")} | |||||
| </Typography>} | |||||
| {/* </div> */} | |||||
| <CustomDatagrid | |||||
| rows={[...rows]} | |||||
| columns={columns} | |||||
| editMode="row" | |||||
| rowModesModel={rowModesModel} | |||||
| onRowEditStop={handleRowEditStop} | |||||
| processRowUpdate={processRowUpdate} | |||||
| // onProcessRowUpdateError={handleProcessRowUpdateError} | |||||
| onRowModesModelChange={handleRowModesModelChange} | |||||
| slots={{ | |||||
| toolbar: EditToolbar, | |||||
| }} | |||||
| slotProps={{ | |||||
| toolbar: { setRows, setRowModesModel }, | |||||
| }} | |||||
| sx={{ | |||||
| height: '100%' | |||||
| }} | |||||
| /> | |||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default ContactDetails; | |||||
| @@ -9,7 +9,6 @@ import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import React, { useCallback, useState } from "react"; | import React, { useCallback, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | |||||
| import { | import { | ||||
| FieldErrors, | FieldErrors, | ||||
| FormProvider, | FormProvider, | ||||
| @@ -17,18 +16,18 @@ import { | |||||
| SubmitHandler, | SubmitHandler, | ||||
| useForm, | useForm, | ||||
| } from "react-hook-form"; | } from "react-hook-form"; | ||||
| import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; | |||||
| import { Error } from "@mui/icons-material"; | import { Error } from "@mui/icons-material"; | ||||
| import { ProjectCategory } from "@/app/api/projects"; | |||||
| import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
| import { CustomerFormInputs, saveCustomer } from "@/app/api/customer/actions"; | import { CustomerFormInputs, saveCustomer } from "@/app/api/customer/actions"; | ||||
| import CustomerDetails from "./CustomerDetails"; | import CustomerDetails from "./CustomerDetails"; | ||||
| import SubsidiaryAllocation from "./SubsidiaryAllocation"; | import SubsidiaryAllocation from "./SubsidiaryAllocation"; | ||||
| import { Subsidiary } from "@/app/api/customer"; | |||||
| import { CustomerType, Subsidiary } from "@/app/api/customer"; | |||||
| import { getDeletedRecordWithRefList } from "@/app/utils/commonUtil"; | import { getDeletedRecordWithRefList } from "@/app/utils/commonUtil"; | ||||
| import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; | |||||
| export interface Props { | export interface Props { | ||||
| subsidiaries: Subsidiary[], | subsidiaries: Subsidiary[], | ||||
| customerTypes: CustomerType[], | |||||
| } | } | ||||
| const hasErrorsInTab = ( | const hasErrorsInTab = ( | ||||
| @@ -37,7 +36,7 @@ const hasErrorsInTab = ( | |||||
| ) => { | ) => { | ||||
| switch (tabIndex) { | switch (tabIndex) { | ||||
| case 0: | case 0: | ||||
| return errors.name; | |||||
| return Object.keys(errors).length > 0; | |||||
| default: | default: | ||||
| false; | false; | ||||
| } | } | ||||
| @@ -45,6 +44,7 @@ const hasErrorsInTab = ( | |||||
| const CreateCustomer: React.FC<Props> = ({ | const CreateCustomer: React.FC<Props> = ({ | ||||
| subsidiaries, | subsidiaries, | ||||
| customerTypes, | |||||
| }) => { | }) => { | ||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| @@ -54,6 +54,10 @@ const CreateCustomer: React.FC<Props> = ({ | |||||
| defaultValues: { | defaultValues: { | ||||
| code: "", | code: "", | ||||
| name: "", | name: "", | ||||
| addContacts: [], | |||||
| addSubsidiaryIds: [], | |||||
| deleteSubsidiaryIds: [], | |||||
| deleteContactIds: [] | |||||
| }, | }, | ||||
| }); | }); | ||||
| @@ -71,27 +75,42 @@ const CreateCustomer: React.FC<Props> = ({ | |||||
| const onSubmit = useCallback<SubmitHandler<CustomerFormInputs>>( | const onSubmit = useCallback<SubmitHandler<CustomerFormInputs>>( | ||||
| async (data) => { | async (data) => { | ||||
| try { | try { | ||||
| if (data.isGridEditing) { | |||||
| warningDialog(t("Please save all the rows before submitting"), t) | |||||
| return false | |||||
| } | |||||
| console.log(data); | console.log(data); | ||||
| let haveError = false | let haveError = false | ||||
| if (data.name.length === 0) { | if (data.name.length === 0) { | ||||
| haveError = true | haveError = true | ||||
| formProps.setError("name", {message: "Name is empty", type: "required"}) | |||||
| formProps.setError("name", { message: "Name is empty", type: "required" }) | |||||
| } | } | ||||
| if (data.code.length === 0) { | if (data.code.length === 0) { | ||||
| haveError = true | haveError = true | ||||
| formProps.setError("code", {message: "Code is empty", type: "required"}) | |||||
| 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)) { | if (data.email && data.email?.length > 0 && !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(data.email)) { | ||||
| haveError = true | haveError = true | ||||
| formProps.setError("email", {message: "Email format is not valid", type: "custom"}) | |||||
| formProps.setError("email", { message: "Email format is not valid", type: "custom" }) | |||||
| } | } | ||||
| if (data.brNo && data.brNo?.length > 0 && !/[0-9]{8}/.test(data.brNo)) { | if (data.brNo && data.brNo?.length > 0 && !/[0-9]{8}/.test(data.brNo)) { | ||||
| haveError = true | haveError = true | ||||
| formProps.setError("brNo", {message: "Br No. format is not valid", type: "custom"}) | |||||
| formProps.setError("brNo", { message: "Br No. format is not valid", type: "custom" }) | |||||
| } | |||||
| 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" }) | |||||
| } | |||||
| 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" }) | |||||
| } | } | ||||
| if (haveError) { | if (haveError) { | ||||
| @@ -100,11 +119,28 @@ const CreateCustomer: React.FC<Props> = ({ | |||||
| return false | return false | ||||
| } | } | ||||
| data.deleteSubsidiaryIds = [] | |||||
| // data.deleteSubsidiaryIds = data.deleteSubsidiaryIds ?? [] | |||||
| // data.addSubsidiaryIds = data.addSubsidiaryIds ?? [] | |||||
| // data.deleteContactIds = data.deleteContactIds ?? [] | |||||
| setServerError(""); | setServerError(""); | ||||
| await saveCustomer(data); | |||||
| router.replace("/customer"); | |||||
| submitDialog(async () => { | |||||
| const response = await saveCustomer(data); | |||||
| if (response.message === "Success") { | |||||
| successDialog(t("Submit Success"), t).then(() => { | |||||
| router.replace("/settings/customer"); | |||||
| }) | |||||
| } else { | |||||
| errorDialog(t("Submit Fail"), t).then(() => { | |||||
| formProps.setError("code", { message: response.message, type: "custom" }) | |||||
| setTabIndex(0) | |||||
| return false | |||||
| }) | |||||
| } | |||||
| }, t) | |||||
| } catch (e) { | } catch (e) { | ||||
| console.log(e) | |||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -114,7 +150,7 @@ const CreateCustomer: React.FC<Props> = ({ | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<CustomerFormInputs>>( | const onSubmitError = useCallback<SubmitErrorHandler<CustomerFormInputs>>( | ||||
| (errors) => { | (errors) => { | ||||
| // Set the tab so that the focus will go there | // Set the tab so that the focus will go there | ||||
| if (errors.name || errors.code) { | |||||
| if (Object.keys(errors).length > 0) { | |||||
| setTabIndex(0); | setTabIndex(0); | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -147,8 +183,8 @@ const CreateCustomer: React.FC<Props> = ({ | |||||
| {serverError} | {serverError} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| {tabIndex === 0 && <CustomerDetails/>} | |||||
| {tabIndex === 1 && <SubsidiaryAllocation subsidiaries={subsidiaries}/>} | |||||
| {tabIndex === 0 && <CustomerDetails customerTypes={customerTypes} />} | |||||
| {tabIndex === 1 && <SubsidiaryAllocation subsidiaries={subsidiaries} />} | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| <Button | <Button | ||||
| @@ -2,17 +2,18 @@ | |||||
| // import CreateProject from "./CreateProject"; | // import CreateProject from "./CreateProject"; | ||||
| // import { fetchProjectCategories } from "@/app/api/projects"; | // import { fetchProjectCategories } from "@/app/api/projects"; | ||||
| // import { fetchTeamLeads } from "@/app/api/staff"; | // import { fetchTeamLeads } from "@/app/api/staff"; | ||||
| import { fetchSubsidiaries } from "@/app/api/customer"; | |||||
| import { fetchCustomerTypes, fetchSubsidiaries } from "@/app/api/customer"; | |||||
| import CreateCustomer from "./CreateCustomer"; | import CreateCustomer from "./CreateCustomer"; | ||||
| const CreateCustomerWrapper: React.FC = async () => { | const CreateCustomerWrapper: React.FC = async () => { | ||||
| const [subsidiaries] = | |||||
| const [subsidiaries, customerTypes] = | |||||
| await Promise.all([ | await Promise.all([ | ||||
| fetchSubsidiaries(), | fetchSubsidiaries(), | ||||
| fetchCustomerTypes(), | |||||
| ]); | ]); | ||||
| return ( | return ( | ||||
| <CreateCustomer subsidiaries={subsidiaries}/> | |||||
| <CreateCustomer subsidiaries={subsidiaries} customerTypes={customerTypes}/> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -11,22 +11,30 @@ import { useTranslation } from "react-i18next"; | |||||
| import CardActions from "@mui/material/CardActions"; | import CardActions from "@mui/material/CardActions"; | ||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import { useFormContext } from "react-hook-form"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { CustomerFormInputs } from "@/app/api/customer/actions"; | 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 { | interface Props { | ||||
| customerTypes: CustomerType[], | |||||
| } | } | ||||
| const CustomerDetails: React.FC<Props> = ({ | const CustomerDetails: React.FC<Props> = ({ | ||||
| customerTypes, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { | const { | ||||
| register, | register, | ||||
| formState: { errors }, | formState: { errors }, | ||||
| control, | |||||
| reset | |||||
| } = useFormContext<CustomerFormInputs>(); | } = useFormContext<CustomerFormInputs>(); | ||||
| return ( | return ( | ||||
| <Card sx={{ display: "block"}}> | |||||
| <> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
| <Box> | <Box> | ||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
| @@ -41,7 +49,7 @@ const CustomerDetails: React.FC<Props> = ({ | |||||
| required: true, | required: true, | ||||
| })} | })} | ||||
| error={Boolean(errors.code)} | error={Boolean(errors.code)} | ||||
| helperText={Boolean(errors.code) && t("Please input correct customer code")} | |||||
| helperText={Boolean(errors.code) && (errors.code?.message ? t(errors.code.message) : t("Please input correct customer code"))} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| @@ -69,7 +77,7 @@ const CustomerDetails: React.FC<Props> = ({ | |||||
| {...register("district")} | {...register("district")} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| {/* <Grid item xs={6}> | |||||
| <TextField | <TextField | ||||
| label={t("Customer Email")} | label={t("Customer Email")} | ||||
| fullWidth | fullWidth | ||||
| @@ -93,6 +101,28 @@ const CustomerDetails: React.FC<Props> = ({ | |||||
| fullWidth | fullWidth | ||||
| {...register("contactName")} | {...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> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| @@ -108,12 +138,14 @@ const CustomerDetails: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| </Box> | </Box> | ||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | <CardActions sx={{ justifyContent: "flex-end" }}> | ||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| <Button onClick={() => reset()} variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | {t("Reset")} | ||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| <ContactDetails/> | |||||
| </> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -99,8 +99,8 @@ const navigationItems: NavigationItem[] = [ | |||||
| { | { | ||||
| icon: <Settings />, label: "Setting", path: "", | icon: <Settings />, label: "Setting", path: "", | ||||
| children: [ | children: [ | ||||
| { icon: <GroupIcon />, label: "Customer", path: "/customer" }, | |||||
| { icon: <Staff />, label: "Staff", path: "/staff" }, | |||||
| { icon: <GroupIcon />, label: "Customer", path: "/settings/customer" }, | |||||
| { icon: <Staff />, label: "Staff", path: "/settings/staff" }, | |||||
| { icon: <Company />, label: "Company", path: "/settings/company" }, | { icon: <Company />, label: "Company", path: "/settings/company" }, | ||||
| { icon: <Department />, label: "Department", path: "/settings/department" }, | { icon: <Department />, label: "Department", path: "/settings/department" }, | ||||
| { icon: <Position />, label: "Position", path: "/settings/position" }, | { icon: <Position />, label: "Position", path: "/settings/position" }, | ||||
| @@ -1,3 +1,5 @@ | |||||
| import React from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| export const msg = (text) => { | export const msg = (text) => { | ||||
| @@ -20,3 +22,46 @@ export const msg = (text) => { | |||||
| export const popup = (text) => { | export const popup = (text) => { | ||||
| Swal.fire(text); | Swal.fire(text); | ||||
| }; | }; | ||||
| export const successDialog = (text, t) => { | |||||
| return Swal.fire({ | |||||
| icon: "success", | |||||
| title: text, | |||||
| confirmButtonText: t("Confirm"), | |||||
| showConfirmButton: true, | |||||
| }) | |||||
| } | |||||
| export const errorDialog = (text, t) => { | |||||
| return Swal.fire({ | |||||
| icon: "error", | |||||
| title: text, | |||||
| confirmButtonText: t("Confirm"), | |||||
| showConfirmButton: true, | |||||
| }) | |||||
| } | |||||
| export const warningDialog = (text, t) => { | |||||
| return Swal.fire({ | |||||
| icon: "warning", | |||||
| title: text, | |||||
| confirmButtonText: t("Confirm"), | |||||
| showConfirmButton: true, | |||||
| }) | |||||
| } | |||||
| export const submitDialog = (confirmAction, t) => { | |||||
| // const { t } = useTranslation("common") | |||||
| return 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() | |||||
| } | |||||
| }) | |||||
| } | |||||
| @@ -5,5 +5,6 @@ | |||||
| "Search Criteria": "Search Criteria", | "Search Criteria": "Search Criteria", | ||||
| "Cancel": "Cancel", | "Cancel": "Cancel", | ||||
| "Confirm": "Confirm", | "Confirm": "Confirm", | ||||
| "Submit": "Submit", | |||||
| "Reset": "Reset" | "Reset": "Reset" | ||||
| } | } | ||||
| @@ -11,11 +11,13 @@ | |||||
| "Customer Contact Name": "Client Contact Name", | "Customer Contact Name": "Client Contact Name", | ||||
| "Customer Br No.": "Client Br No.", | "Customer Br No.": "Client Br No.", | ||||
| "Customer Details": "Client Details", | "Customer Details": "Client Details", | ||||
| "Customer Type": "Client Type", | |||||
| "Please input correct customer code": "Please input correct client code", | "Please input correct customer code": "Please input correct client code", | ||||
| "Please input correct customer name": "Please input correct client name", | "Please input correct customer name": "Please input correct client name", | ||||
| "Please input correct customer email": "Please input correct client email", | "Please input correct customer email": "Please input correct client email", | ||||
| "Please input correct customer br no.": "Please input correct client br no.", | "Please input correct customer br no.": "Please input correct client br no.", | ||||
| "The customer code has already existed": "The customer code has already existed", | |||||
| "Subsidiary" : "Subsidiary", | "Subsidiary" : "Subsidiary", | ||||
| "Subsidiary Allocation": "Subsidiary Allocation", | "Subsidiary Allocation": "Subsidiary Allocation", | ||||
| @@ -32,11 +34,23 @@ | |||||
| "Subsidiary Br No.": "Subsidiary Br No.", | "Subsidiary Br No.": "Subsidiary Br No.", | ||||
| "Subsidiary Details": "Subsidiary Details", | "Subsidiary Details": "Subsidiary Details", | ||||
| "Add Contact Person": "Add Contact Person", | |||||
| "Contact Details": "Contact Details", | |||||
| "Contact Name": "Contact Name", | |||||
| "Contact Email": "Contact Email", | |||||
| "Contact Phone": "Contact Phone", | |||||
| "Please ensure all the fields are inputted and saved": "Please ensure all the fields are inputted and saved", | |||||
| "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", | |||||
| "Do you want to submit?": "Do you want to submit?", | |||||
| "Submit Success": "Submit Success", | |||||
| "Add": "Add", | "Add": "Add", | ||||
| "Details": "Details", | "Details": "Details", | ||||
| "Search": "Search", | "Search": "Search", | ||||
| "Search Criteria": "Search Criteria", | "Search Criteria": "Search Criteria", | ||||
| "Cancel": "Cancel", | "Cancel": "Cancel", | ||||
| "Confirm": "Confirm", | "Confirm": "Confirm", | ||||
| "Submit": "Submit", | |||||
| "Reset": "Reset" | "Reset": "Reset" | ||||
| } | } | ||||
| @@ -3,5 +3,6 @@ | |||||
| "Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
| "Cancel": "取消", | "Cancel": "取消", | ||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Submit": "提交", | |||||
| "Reset": "重置" | "Reset": "重置" | ||||
| } | } | ||||
| @@ -11,11 +11,13 @@ | |||||
| "Customer Contact Name": "客戶聯絡名稱", | "Customer Contact Name": "客戶聯絡名稱", | ||||
| "Customer Br No.": "客戶商業登記號碼", | "Customer Br No.": "客戶商業登記號碼", | ||||
| "Customer Details": "客戶詳請", | "Customer Details": "客戶詳請", | ||||
| "Customer Type": "客戶類型", | |||||
| "Please input correct customer code": "請輸入客戶編號", | "Please input correct customer code": "請輸入客戶編號", | ||||
| "Please input correct customer name": "請輸入客戶編號", | "Please input correct customer name": "請輸入客戶編號", | ||||
| "Please input correct customer email": "請輸入正確客戶電郵", | "Please input correct customer email": "請輸入正確客戶電郵", | ||||
| "Please input correct customer br no.": "請輸入正確客戶商業登記號碼", | "Please input correct customer br no.": "請輸入正確客戶商業登記號碼", | ||||
| "The customer code has already existed": "該客戶編號已存在", | |||||
| "Subsidiary": "子公司", | "Subsidiary": "子公司", | ||||
| "Subsidiary Allocation": "子公司分配", | "Subsidiary Allocation": "子公司分配", | ||||
| @@ -32,11 +34,23 @@ | |||||
| "Subsidiary Br No.": "子公司商業登記號碼", | "Subsidiary Br No.": "子公司商業登記號碼", | ||||
| "Subsidiary Details": "子公司詳請", | "Subsidiary Details": "子公司詳請", | ||||
| "Add Contact Person": "新增聯絡人", | |||||
| "Contact Details": "聯絡詳請", | |||||
| "Contact Name": "聯絡姓名", | |||||
| "Contact Email": "聯絡電郵", | |||||
| "Contact Phone": "聯絡電話", | |||||
| "Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存", | |||||
| "Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | |||||
| "Do you want to submit?": "你是否確認要提交?", | |||||
| "Submit Success": "提交成功", | |||||
| "Add": "新增", | "Add": "新增", | ||||
| "Details": "詳請", | "Details": "詳請", | ||||
| "Search": "搜尋", | "Search": "搜尋", | ||||
| "Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
| "Cancel": "取消", | "Cancel": "取消", | ||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Submit": "提交", | |||||
| "Reset": "重置" | "Reset": "重置" | ||||
| } | } | ||||