diff --git a/src/app/(main)/settings/customer/create/page.tsx b/src/app/(main)/settings/customer/create/page.tsx index 99bc845..c4f13b4 100644 --- a/src/app/(main)/settings/customer/create/page.tsx +++ b/src/app/(main)/settings/customer/create/page.tsx @@ -1,4 +1,3 @@ -import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; import CustomerDetail from "@/components/CustomerDetail"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; @@ -12,7 +11,6 @@ export const metadata: Metadata = { const CreateCustomer: React.FC = async () => { const { t } = await getServerI18n("customer"); - // fetchSubsidiaries(); return ( <> diff --git a/src/app/(main)/settings/customer/edit/page.tsx b/src/app/(main)/settings/customer/edit/page.tsx index 270194e..004781b 100644 --- a/src/app/(main)/settings/customer/edit/page.tsx +++ b/src/app/(main)/settings/customer/edit/page.tsx @@ -1,4 +1,4 @@ -import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; +import { fetchAllSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; import CustomerDetail from "@/components/CustomerDetail"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; @@ -12,7 +12,7 @@ export const metadata: Metadata = { const EditCustomer: React.FC = async () => { const { t } = await getServerI18n("customer"); - // fetchSubsidiaries(); + // fetchAllSubsidiaries(); return ( <> diff --git a/src/app/(main)/settings/subsidiary/create/page.tsx b/src/app/(main)/settings/subsidiary/create/page.tsx new file mode 100644 index 0000000..967384e --- /dev/null +++ b/src/app/(main)/settings/subsidiary/create/page.tsx @@ -0,0 +1,23 @@ +import SubsidiaryDetail from "@/components/SubsidiaryDetail"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Create Subsidiary", +}; + +const CreateSubsidiary: React.FC = async () => { + const { t } = await getServerI18n("subsidiary"); + + return ( + <> + {t("Create Subsidiary")} + + + + + ); +}; + +export default CreateSubsidiary; diff --git a/src/app/(main)/settings/subsidiary/edit/page.tsx b/src/app/(main)/settings/subsidiary/edit/page.tsx new file mode 100644 index 0000000..e9c50dc --- /dev/null +++ b/src/app/(main)/settings/subsidiary/edit/page.tsx @@ -0,0 +1,23 @@ +import { I18nProvider, getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import SubsidiaryDetail from "@/components/SubsidiaryDetail"; + +export const metadata: Metadata = { + title: "Edit Subsidiary", +}; + +const EditSubsidiary: React.FC = async () => { + const { t } = await getServerI18n("subsidiary"); + + return ( + <> + {t("Edit Subsidiary")} + + + + + ); +}; + +export default EditSubsidiary; diff --git a/src/app/(main)/settings/subsidiary/page.tsx b/src/app/(main)/settings/subsidiary/page.tsx new file mode 100644 index 0000000..cadb670 --- /dev/null +++ b/src/app/(main)/settings/subsidiary/page.tsx @@ -0,0 +1,50 @@ +import { getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; +import { preloadAllSubsidiaries } from "@/app/api/subsidiary"; +import SubsidiarySearch from "@/components/SubsidiarySearch"; + +export const metadata: Metadata = { + title: "Subsidiary", +}; + +const Subsidiary: React.FC = async () => { + const { t } = await getServerI18n("subsidiary"); + preloadAllSubsidiaries(); + + return ( + <> + + + {t("Subsidiary")} + + + + + }> + + + + + ); +}; + +export default Subsidiary; diff --git a/src/app/api/customer/index.ts b/src/app/api/customer/index.ts index bb2f407..7ed4359 100644 --- a/src/app/api/customer/index.ts +++ b/src/app/api/customer/index.ts @@ -23,6 +23,11 @@ export interface CustomerType { name: string; } +export interface SubsidiaryType { + id: number; + name: string; +} + export interface Subsidiary { id: number; code: string; @@ -34,6 +39,21 @@ export interface Subsidiary { address: string | null; district: string | null; email: string | null; + subsidiaryType: SubsidiaryType; +} + +export interface SubsidiaryTable { + id: number; + code: string; + name: string; + description: string | null; + brNo: string | null; + contactName: string | null; + phone: string | null; + address: string | null; + district: string | null; + email: string | null; + subsidiaryType: string; } export interface Contact { @@ -52,7 +72,7 @@ export const fetchAllCustomers = cache(async () => { return serverFetchJson(`${BASE_API_URL}/customer`); }); -export const fetchSubsidiaries = cache(async () => { +export const fetchAllSubsidiaries = cache(async () => { return serverFetchJson(`${BASE_API_URL}/subsidiary`, { next: { tags: ["subsidiary"] }, }); diff --git a/src/app/api/subsidiary/actions.ts b/src/app/api/subsidiary/actions.ts new file mode 100644 index 0000000..7e4d6fb --- /dev/null +++ b/src/app/api/subsidiary/actions.ts @@ -0,0 +1,74 @@ +"use server"; + +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { Contact, Subsidiary, SaveSubsidiaryResponse, } from "."; +import { revalidateTag } from "next/cache"; + +export interface SubsidiaryFormInputs { + + // Subsidiary info + id: number | null; + name: string; + code: string; + address: string | null; + district: string | null; + brNo: string | null; + typeId: number; + + // Customer + addCustomerIds: number[]; + deleteCustomerIds: number[]; + + // Contact + addContacts: Contact[]; + deleteContactIds: number[]; + + // is grid editing + isGridEditing: boolean | null; +} + +export interface SubsidiaryResponse { + subsidiary: Subsidiary; + customerIds: number[]; + contacts: Contact[]; +} + +export const saveSubsidiary = async (data: SubsidiaryFormInputs) => { + const saveSubsidiary = await serverFetchJson( + `${BASE_API_URL}/subsidiary/save`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + revalidateTag("subsidiaries"); + + return saveSubsidiary; +}; + +export const fetchSubsidiary = async (id: number) => { + const subsidiary = await serverFetchJson( + `${BASE_API_URL}/subsidiary/${id}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + return subsidiary +}; + +export const deleteSubsidiary = async (id: number) => { + const subsidiary = await serverFetchWithNoContent( + `${BASE_API_URL}/subsidiary/${id}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }, + ); + + return subsidiary +}; \ No newline at end of file diff --git a/src/app/api/subsidiary/index.ts b/src/app/api/subsidiary/index.ts new file mode 100644 index 0000000..c83f4db --- /dev/null +++ b/src/app/api/subsidiary/index.ts @@ -0,0 +1,83 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + +export interface Customer { + id: number; + code: string; + name: string; + brNo: string | null; + address: string | null; + district: string | null; + customerType: CustomerType +} + +export interface CustomerTable { + id: number; + code: string; + name: string; + brNo: string | null; + address: string | null; + district: string | null; + customerType: string +} + +export interface CustomerType { + id: number; + name: string; +} + +export interface SubsidiaryType { + id: number; + name: string; +} + +export interface Subsidiary { + id: number; + code: string; + name: string; + brNo: string | null; + address: string | null; + district: string | null; + subsidiaryType: SubsidiaryType +} + +export interface SaveSubsidiaryResponse { + subsidiary: Subsidiary; + message: string; +} + +export interface Contact { + id: number; + name: string; + phone: string; + email: string; + isNew: boolean; +} + +export const preloadAllSubsidiaries = () => { + fetchAllSubsidiaries(); +}; + +export const fetchAllCustomers = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/customer`); +}); + +export const fetchAllSubsidiaries = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/subsidiary`, + { + next: { tags: ["subsidiary"] }, + }, + ); +}); + +export const fetchSubsidiaryTypes = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/subsidiary/types`, + { + next: { tags: ["subsidiaryTypes"] }, + }, + ); +}); \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index 480c9fe..e69de29 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -1,7 +0,0 @@ -export function getDeletedRecordWithRefList(referenceList: Array, updatedList: Array) { - return referenceList.filter(x => !updatedList.includes(x)); -} - -export function getNewRecordWithRefList(referenceList: Array, updatedList: Array) { - return updatedList.filter(x => !referenceList.includes(x)); -} diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 091f6f3..0616278 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -17,6 +17,9 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/customer": "Customer", "/settings/customer/create": "Create Customer", "/settings/customer/edit": "Edit Customer", + "/settings/subsidiary": "Subsidiary", + "/settings/subsidiary/create": "Create Subsidiary", + "/settings/subsidiary/edit": "Edit Subsidiary", "/settings": "Settings", "/company": "Company", "/settings/department": "Department", diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerDetail/ContactInfo.tsx index ba722c6..9566264 100644 --- a/src/components/CustomerDetail/ContactInfo.tsx +++ b/src/components/CustomerDetail/ContactInfo.tsx @@ -88,7 +88,6 @@ const ContactInfo: React.FC = ({ useEffect(() => { if (initialRows.length > 0 && rows.length === 0) { - console.log("first") setRows(initialRows) } }, [initialRows.length > 0]) @@ -220,7 +219,7 @@ const ContactInfo: React.FC = ({ // check error useEffect(() => { - if (getValues("addContacts") !== undefined || getValues("addContacts") !== null) { + if (getValues("addContacts") === undefined || getValues("addContacts") === null) { return; } @@ -233,7 +232,6 @@ const ContactInfo: React.FC = ({ 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 { @@ -241,7 +239,7 @@ const ContactInfo: React.FC = ({ } } } - }, [rows]) + }, [rows, rowModesModel]) // check editing useEffect(() => { diff --git a/src/components/CustomerDetail/CustomerDetail.tsx b/src/components/CustomerDetail/CustomerDetail.tsx index bfc85b0..47b47c0 100644 --- a/src/components/CustomerDetail/CustomerDetail.tsx +++ b/src/components/CustomerDetail/CustomerDetail.tsx @@ -22,8 +22,8 @@ import { CustomerFormInputs, fetchCustomer, saveCustomer } from "@/app/api/custo import CustomerInfo from "./CustomerInfo"; import SubsidiaryAllocation from "./SubsidiaryAllocation"; import { CustomerType, Subsidiary } from "@/app/api/customer"; -import { getDeletedRecordWithRefList } from "@/app/utils/commonUtil"; import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; +import { differenceBy } from "lodash"; export interface Props { subsidiaries: Subsidiary[], @@ -80,9 +80,9 @@ const CustomerDetail: React.FC = ({ id: customer.customer.id, code: customer.customer.code ?? "", name: customer.customer.name ?? "", - brNo: customer.customer.brNo ?? "", - address: customer.customer.address ?? "", - district: customer.customer.district ?? "", + brNo: customer.customer.brNo ?? null, + address: customer.customer.address ?? null, + district: customer.customer.district ?? null, typeId: customer.customer.customerType.id, addContacts: customer.contacts ?? [], addSubsidiaryIds: customer.subsidiaryIds ?? [], @@ -177,12 +177,12 @@ const CustomerDetail: React.FC = ({ 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 info include empty fields", type: "required" }) + formProps.setError("addContacts", { message: "Contact info includes 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 info include empty fields", type: "email_format" }) + formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" }) } if (haveError) { @@ -191,8 +191,8 @@ const CustomerDetail: React.FC = ({ return false } - data.deleteContactIds = getDeletedRecordWithRefList(refCustomer?.addContacts.map(contact => contact.id)!!, data.addContacts.map(contact => contact.id)!!) - data.deleteSubsidiaryIds = getDeletedRecordWithRefList(refCustomer?.addSubsidiaryIds!!, data.addSubsidiaryIds) + data.deleteContactIds = differenceBy(refCustomer?.addContacts.map(contact => contact.id)!!, data.addContacts.map(contact => contact.id)!!) + data.deleteSubsidiaryIds = differenceBy(refCustomer?.addSubsidiaryIds!!, data.addSubsidiaryIds) setServerError(""); diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerDetail/CustomerDetailWrapper.tsx index 0e1b3e1..f8d0d12 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerDetail/CustomerDetailWrapper.tsx @@ -2,9 +2,8 @@ // import CreateProject from "./CreateProject"; // import { fetchProjectCategories } from "@/app/api/projects"; // import { fetchTeamLeads } from "@/app/api/staff"; -import { fetchCustomerTypes, fetchSubsidiaries } from "@/app/api/customer"; +import { Subsidiary, fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; import CustomerDetail from "./CustomerDetail"; -import { getServerSideProps } from "next/dist/build/templates/pages"; // type Props = { // params: { @@ -17,7 +16,7 @@ const CustomerDetailWrapper: React.FC = async () => { // console.log(params) const [subsidiaries, customerTypes] = await Promise.all([ - fetchSubsidiaries(), + fetchAllSubsidiaries(), fetchCustomerTypes(), ]); diff --git a/src/components/CustomerDetail/SubsidiaryAllocation.tsx b/src/components/CustomerDetail/SubsidiaryAllocation.tsx index 2b54294..ff728e6 100644 --- a/src/components/CustomerDetail/SubsidiaryAllocation.tsx +++ b/src/components/CustomerDetail/SubsidiaryAllocation.tsx @@ -24,7 +24,7 @@ import { import differenceBy from "lodash/differenceBy"; import { useFormContext } from "react-hook-form"; import { CustomerFormInputs } from "@/app/api/customer/actions"; -import { Subsidiary } from "@/app/api/customer"; +import { Subsidiary, SubsidiaryTable } from "@/app/api/customer"; interface Props { subsidiaries: Subsidiary[]; @@ -36,21 +36,21 @@ const SubsidiaryAllocation: React.FC = ({ const { t } = useTranslation(); const { setValue, getValues, formState: { defaultValues }, reset, resetField } = useFormContext(); - const [filteredSubsidiary, setFilteredSubsidiary] = React.useState(subsidiaries); + const initialSubsidiaries = subsidiaries.map(subsidiary => ({...subsidiary, subsidiaryType: subsidiary.subsidiaryType.name})) + const [filteredSubsidiary, setFilteredSubsidiary] = React.useState(initialSubsidiaries); const [selectedSubsidiary, setSelectedSubsidiary] = React.useState< typeof filteredSubsidiary - >( - subsidiaries.filter((subsidiary) => + >(initialSubsidiaries.filter((subsidiary) => getValues("addSubsidiaryIds")?.includes(subsidiary.id), ) ); // Adding / Removing staff - const addSubsidiary = React.useCallback((subsidiary: Subsidiary) => { + const addSubsidiary = React.useCallback((subsidiary: SubsidiaryTable) => { setSelectedSubsidiary((subsidiaries) => [...subsidiaries, subsidiary]); }, []); - const removeSubsidiary = React.useCallback((subsidiary: Subsidiary) => { + const removeSubsidiary = React.useCallback((subsidiary: SubsidiaryTable) => { setSelectedSubsidiary((subsidiaries) => subsidiaries.filter((s) => s.id !== subsidiary.id)); }, []); @@ -58,7 +58,7 @@ const SubsidiaryAllocation: React.FC = ({ if (defaultValues !== undefined) { // reset({ addSubsidiaryIds: defaultValues.addSubsidiaryIds }) resetField("addSubsidiaryIds") - setSelectedSubsidiary(subsidiaries.filter((subsidiary) => + setSelectedSubsidiary(initialSubsidiaries.filter((subsidiary) => defaultValues.addSubsidiaryIds?.includes(subsidiary.id), )) } @@ -72,7 +72,7 @@ const SubsidiaryAllocation: React.FC = ({ ); }, [selectedSubsidiary, setValue]); - const subsidiaryPoolColumns = React.useMemo[]>( + const subsidiaryPoolColumns = React.useMemo[]>( () => [ { label: t("Add"), @@ -88,11 +88,12 @@ const SubsidiaryAllocation: React.FC = ({ { label: t("Subsidiary Address"), name: "address" }, { label: t("Subsidiary District"), name: "district" }, // { label: t("Subsidiary Email"), name: "email" }, + { label: t("Subsidiary Type"), name: "subsidiaryType" }, ], [addSubsidiary, t], ); - const allocatedSubsidiaryColumns = React.useMemo[]>( + const allocatedSubsidiaryColumns = React.useMemo[]>( () => [ { label: t("Remove"), @@ -108,6 +109,7 @@ const SubsidiaryAllocation: React.FC = ({ { label: t("Subsidiary Address"), name: "address" }, { label: t("Subsidiary District"), name: "district" }, // { label: t("Subsidiary Email"), name: "email" }, + { label: t("Subsidiary Type"), name: "subsidiaryType" }, ], [removeSubsidiary, t], ); @@ -125,7 +127,7 @@ const SubsidiaryAllocation: React.FC = ({ React.useEffect(() => { setFilteredSubsidiary( - subsidiaries.filter((subsidiary) => { + initialSubsidiaries.filter((subsidiary) => { const q = query.toLowerCase(); return ( (subsidiary.name.toLowerCase().includes(q) || diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index f80697f..b615fd5 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -29,6 +29,8 @@ import Link from "next/link"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; import GroupIcon from '@mui/icons-material/Group'; +import BusinessIcon from '@mui/icons-material/Business'; + interface NavigationItem { icon: React.ReactNode; label: string; @@ -100,6 +102,7 @@ const navigationItems: NavigationItem[] = [ icon: , label: "Setting", path: "", children: [ { icon: , label: "Customer", path: "/settings/customer" }, + { icon: , label: "Subsidiary", path: "/settings/subsidiary" }, { icon: , label: "Staff", path: "/settings/staff" }, { icon: , label: "Company", path: "/settings/company" }, { icon: , label: "Department", path: "/settings/department" }, diff --git a/src/components/SubsidiaryDetail/ContactInfo.tsx b/src/components/SubsidiaryDetail/ContactInfo.tsx new file mode 100644 index 0000000..9e2c16d --- /dev/null +++ b/src/components/SubsidiaryDetail/ContactInfo.tsx @@ -0,0 +1,302 @@ +"use client"; + +import Stack from "@mui/material/Stack"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +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, + GridColDef, + GridToolbarContainer, + GridActionsCellItem, + GridEventListener, + GridRowId, + GridRowModel, + GridRowEditStopReasons, +} from '@mui/x-data-grid'; +import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; +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 ( + + + + ); +} + +const ContactInfo: React.FC = ({ +}) => { + const { t } = useTranslation(); + + const { control, setValue, getValues, formState: { errors, defaultValues }, setError, clearErrors, reset, watch, resetField } = 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([]); + const [rowModesModel, setRowModesModel] = useState({}); + + useEffect(() => { + if (initialRows.length > 0 && rows.length === 0) { + setRows(initialRows) + } + }, [initialRows.length > 0]) + + 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 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( + () => [ + { + 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 [ + } + label="Save" + sx={{ + color: 'primary.main', + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={handleDeleteClick(id)} + color="inherit" + />, + ]; + }, + }, + ], + [rows, rowModesModel, t], + ); + + // check error + useEffect(() => { + if (getValues("addContacts") === undefined || getValues("addContacts") === null) { + return; + } + + 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 ( + + + + {/*
*/} + + {t("Contact Info")} + + {Boolean(errors.addContacts?.type === "required") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> + {t("Please ensure all the fields are inputted and saved")} + } + {Boolean(errors.addContacts?.type === "email_format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> + {t("Please ensure all the email formats are correct")} + } + {/*
*/} + + + + +
+
+
+ ); +}; + +export default ContactInfo; \ No newline at end of file diff --git a/src/components/SubsidiaryDetail/CustomerAllocation.tsx b/src/components/SubsidiaryDetail/CustomerAllocation.tsx new file mode 100644 index 0000000..53ba804 --- /dev/null +++ b/src/components/SubsidiaryDetail/CustomerAllocation.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import React, { useEffect } from "react"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import SearchResults, { Column } from "../SearchResults"; +import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; +import { + Stack, + Typography, + Grid, + TextField, + InputAdornment, + IconButton, + Box, + Button, + Card, + CardActions, + CardContent, + TabsProps, + Tab, + Tabs, +} from "@mui/material"; +import differenceBy from "lodash/differenceBy"; +import { useFormContext } from "react-hook-form"; +import { Customer, CustomerTable } from "@/app/api/subsidiary"; +import { SubsidiaryFormInputs } from "@/app/api/subsidiary/actions"; + +interface Props { + customers: Customer[]; +} + +const CustomerAllocation: React.FC = ({ + customers, +}) => { + const { t } = useTranslation(); + const { setValue, getValues, formState: { defaultValues }, reset, resetField } = useFormContext(); + + const initialCustomers = customers.map(customer => ({...customer, customerType: customer.customerType.name})) + const [filteredCustomer, setFilteredCustomer] = React.useState(initialCustomers); + const [selectedCustomer, setSelectedCustomer] = React.useState< + typeof filteredCustomer + >(initialCustomers.filter((customer) => + getValues("addCustomerIds")?.includes(customer.id), + ) + ); + + // Adding / Removing customer + const addCustomer = React.useCallback((customer: CustomerTable) => { + setSelectedCustomer((customers) => [...customers, customer]); + }, []); + + const removeCustomer = React.useCallback((customer: CustomerTable) => { + setSelectedCustomer((customers) => customers.filter((customers) => customers.id !== customer.id)); + }, []); + + const clearCustomer = React.useCallback(() => { + if (defaultValues !== undefined) { + // reset({ addSubsidiaryIds: defaultValues.addSubsidiaryIds }) + resetField("addCustomerIds") + setSelectedCustomer(initialCustomers.filter((customer) => + defaultValues.addCustomerIds?.includes(customer.id), + )) + } + }, [defaultValues]); + + // Sync with form + useEffect(() => { + setValue( + "addCustomerIds", + selectedCustomer.map((customer) => customer.id), + ); + }, [selectedCustomer, setValue]); + + const customerPoolColumns = React.useMemo[]>( + () => [ + { + label: t("Add"), + name: "id", + onClick: addCustomer, + buttonIcon: , + }, + { label: t("Customer Code"), name: "code" }, + { label: t("Customer Name"), name: "name" }, + { label: t("Customer Br No."), name: "brNo" }, + { label: t("Customer Address"), name: "address" }, + { label: t("Customer District"), name: "district" }, + { label: t("Customer Type"), name: "customerType" }, + ], + [addCustomer, t], + ); + + const allocatedCustomerColumns = React.useMemo[]>( + () => [ + { + label: t("Remove"), + name: "id", + onClick: removeCustomer, + buttonIcon: , + }, + { label: t("Customer Code"), name: "code" }, + { label: t("Customer Name"), name: "name" }, + { label: t("Customer Br No."), name: "brNo" }, + { label: t("Customer Address"), name: "address" }, + { label: t("Customer District"), name: "district" }, + { label: t("Customer Type"), name: "customerType" }, + ], + [removeCustomer, t], + ); + + // Query related + const [query, setQuery] = React.useState(""); + const onQueryInputChange = React.useCallback< + React.ChangeEventHandler + >((e) => { + setQuery(e.target.value); + }, []); + const clearQueryInput = React.useCallback(() => { + setQuery(""); + }, []); + + React.useEffect(() => { + setFilteredCustomer( + initialCustomers.filter((customer) => { + const q = query.toLowerCase(); + return ( + (customer.name.toLowerCase().includes(q) || + customer.code.toString().includes(q) || + (customer.brNo != null && customer.brNo.toLowerCase().includes(q))) + ); + }), + ); + }, [customers, query]); + + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], + ); + + const resetCustomer = React.useCallback(() => { + clearQueryInput(); + clearCustomer(); + }, [clearQueryInput, clearCustomer]); + + return ( + <> + + + + + {t("Customer Allocation")} + + + + + + + + + + ), + }} + /> + + + + + + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + + + + + + + + + ); +}; + +export default CustomerAllocation; \ No newline at end of file diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx new file mode 100644 index 0000000..c458ce1 --- /dev/null +++ b/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx @@ -0,0 +1,256 @@ +"use client"; + +import Check from "@mui/icons-material/Check"; +import Close from "@mui/icons-material/Close"; +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, useSearchParams } from "next/navigation"; +import React, { useCallback, useEffect, useLayoutEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { Error } from "@mui/icons-material"; +import { Typography } from "@mui/material"; +import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; +import { Customer, SubsidiaryType } from "@/app/api/subsidiary"; +import { SubsidiaryFormInputs, fetchSubsidiary, saveSubsidiary } from "@/app/api/subsidiary/actions"; +import { differenceBy } from "lodash"; +import SubsidiaryInfo from "./SubsidiaryInfo"; +import CustomerAllocation from "./CustomerAllocation"; + +export interface Props { + customers: Customer[], + subsidiaryTypes: SubsidiaryType[], +} + +const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors, +) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } +}; + +const SubsidiaryDetail: React.FC = ({ + customers, + subsidiaryTypes, +}) => { + const [serverError, setServerError] = useState(""); + const [tabIndex, setTabIndex] = useState(0); + const [refSubsidiary, setRefSubsidiary] = useState() + const { t } = useTranslation(); + const router = useRouter(); + const searchParams = useSearchParams() + + const fetchCurrentSubsidiary = async () => { + const id = searchParams.get('id') + try { + const defaultSubsidiary = { + id: null, + code: "", + name: "", + brNo: null, + address: null, + district: null, + typeId: 1, + addContacts: [], + addCustomerIds: [], + deleteCustomerIds: [], + deleteContactIds: [], + isGridEditing: false + } + + if (id !== null && parseInt(id) > 0) { + + const subsidiary = await fetchSubsidiary(parseInt(id)) + + if (subsidiary !== null && Object.keys(subsidiary).length > 0) { + console.log(subsidiary) + const tempSubsidiaryInput = { + id: subsidiary.subsidiary.id, + code: subsidiary.subsidiary.code ?? "", + name: subsidiary.subsidiary.name ?? "", + brNo: subsidiary.subsidiary.brNo ?? null, + address: subsidiary.subsidiary.address ?? null, + district: subsidiary.subsidiary.district ?? null, + typeId: subsidiary.subsidiary.subsidiaryType.id, + addContacts: subsidiary.contacts ?? [], + addCustomerIds: subsidiary.customerIds ?? [], + deleteCustomerIds: [], + deleteContactIds: [], + isGridEditing: false + } + setRefSubsidiary(tempSubsidiaryInput) + } else { + setRefSubsidiary(defaultSubsidiary) + } + } else { + setRefSubsidiary(defaultSubsidiary) + } + } catch (e) { + console.log(e) + setServerError(t("An error has occurred. Please try again later.")); + } + } + + useLayoutEffect(() => { + fetchCurrentSubsidiary() + }, []) + + const formProps = useForm(); + + useEffect(() => { + if (refSubsidiary !== null && refSubsidiary !== undefined) { + formProps.reset(refSubsidiary) + } + }, [refSubsidiary]) + + const handleCancel = () => { + router.back(); + }; + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], + ); + + const onSubmit = useCallback>( + async (data) => { + try { + if (data.isGridEditing) { + warningDialog(t("Please save all the rows before submitting"), t) + return false + } + + console.log(data); + + let haveError = false + if (data.name.length === 0) { + haveError = true + formProps.setError("name", { message: "Name is empty", type: "required" }) + } + + if (data.code.length === 0) { + haveError = true + formProps.setError("code", { message: "Code is empty", type: "required" }) + } + + if (data.brNo && data.brNo?.length > 0 && !/[0-9]{8}/.test(data.brNo)) { + haveError = true + 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 info includes 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 info includes invalid email", type: "email_format" }) + } + + if (haveError) { + // go to the error tab + setTabIndex(0) + return false + } + + data.deleteContactIds = differenceBy(refSubsidiary?.addContacts.map(contact => contact.id)!!, data.addContacts.map(contact => contact.id)!!) + data.deleteCustomerIds = differenceBy(refSubsidiary?.addCustomerIds!!, data.addCustomerIds) + + setServerError(""); + + submitDialog(async () => { + const response = await saveSubsidiary(data); + + if (response.message === "Success") { + successDialog(t("Submit Success"), t).then(() => { + router.replace("/settings/subsidiary"); + }) + } else { + errorDialog(t("Submit Fail"), t).then(() => { + formProps.setError("code", { message: response.message, type: "custom" }) + setTabIndex(0) + return false + }) + } + }, t) + } catch (e) { + console.log(e) + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router, t, refSubsidiary], + ); + + const onSubmitError = useCallback>( + (errors) => { + // Set the tab so that the focus will go there + if (Object.keys(errors).length > 0) { + setTabIndex(0); + } + }, + [], + ); + + const errors = formProps.formState.errors; + + return ( + + {refSubsidiary && + + + ) : undefined + } + iconPosition="end" + /> + + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + {tabIndex === 1 && } + + + + + + } + + ); +}; + +export default SubsidiaryDetail; \ No newline at end of file diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx new file mode 100644 index 0000000..1a9ced0 --- /dev/null +++ b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx @@ -0,0 +1,16 @@ +import { fetchAllCustomers, fetchSubsidiaryTypes } from "@/app/api/subsidiary"; +import SubsidiaryDetail from "./SubsidiaryDetail"; + +const CustomerDetailWrapper: React.FC = async () => { + const [customers, subsidiaryTypes] = + await Promise.all([ + fetchAllCustomers(), + fetchSubsidiaryTypes(), + ]); + + return ( + + ); +}; + +export default CustomerDetailWrapper; diff --git a/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx b/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx new file mode 100644 index 0000000..84522a4 --- /dev/null +++ b/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx @@ -0,0 +1,141 @@ +"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 { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import ContactInfo from "./ContactInfo"; +import { useCallback } from "react"; +import { SubsidiaryType } from "@/app/api/subsidiary"; + +interface Props { + subsidiaryTypes: SubsidiaryType[], +} + +const SubsidiaryInfo: React.FC = ({ + subsidiaryTypes, +}) => { + const { t } = useTranslation(); + const { + register, + formState: { errors, defaultValues }, + control, + reset, + resetField, + setValue + } = useFormContext(); + + const resetSubsidiary = useCallback(() => { + if (defaultValues !== undefined) { + resetField("code") + resetField("name") + resetField("address") + resetField("district") + resetField("typeId") + resetField("brNo") + } + }, [defaultValues]) + + return ( + <> + + + + + {t("Subsidiary Info")} + + + + + + + + + + + + + + + + + {t("Subsidiary Type")} + ( + + )} + /> + + + + + + + + + + + + + + + ); +}; + +export default SubsidiaryInfo; diff --git a/src/components/SubsidiaryDetail/index.ts b/src/components/SubsidiaryDetail/index.ts new file mode 100644 index 0000000..66405c1 --- /dev/null +++ b/src/components/SubsidiaryDetail/index.ts @@ -0,0 +1 @@ +export { default } from "./SubsidiaryDetailWrapper"; \ No newline at end of file diff --git a/src/components/SubsidiarySearch/SubsidiarySearch.tsx b/src/components/SubsidiarySearch/SubsidiarySearch.tsx new file mode 100644 index 0000000..26dbbdb --- /dev/null +++ b/src/components/SubsidiarySearch/SubsidiarySearch.tsx @@ -0,0 +1,95 @@ +"use client"; + +import React, { useCallback, useMemo, useState } from "react"; +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 { Subsidiary } from "@/app/api/subsidiary"; +import { deleteSubsidiary } from "@/app/api/subsidiary/actions"; + +interface Props { + subsidiaries: Subsidiary[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const SubsidiarySearch: React.FC = ({ subsidiaries }) => { + const { t } = useTranslation(); + const router = useRouter() + const searchParams = useSearchParams() + + const [filteredSubsidiaries, setFilteredSubsidiaries] = useState(subsidiaries); + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Subsidiary Code"), paramName: "code", type: "text" }, + { label: t("Subsidiary Name"), paramName: "name", type: "text" }, + ], + [t], + ); + const onReset = useCallback(() => { + setFilteredSubsidiaries(subsidiaries); + }, [subsidiaries]); + + const onTaskClick = useCallback((subsidiary: Subsidiary) => { + const params = new URLSearchParams(searchParams.toString()) + params.set("id", subsidiary.id.toString()) + router.replace(`/settings/subsidiary/edit?${params.toString()}`); + }, []); + + const onDeleteClick = useCallback((subsidiary: Subsidiary) => { + + deleteDialog(async() => { + await deleteSubsidiary(subsidiary.id) + + successDialog("Delete Success", t) + + setFilteredSubsidiaries((prev) => prev.filter((obj) => obj.id !== subsidiary.id)) + }, t) + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onTaskClick, + buttonIcon: , + }, + { name: "code", label: t("Subsidiary Code") }, + { name: "name", label: t("Subsidiary Name") }, + { + name: "id", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + }, + ], + [onTaskClick, t], + ); + + return ( + <> + { + setFilteredSubsidiaries( + subsidiaries.filter( + (subsidiary) => + subsidiary.code.toLowerCase().includes(query.code.toLowerCase()) && + subsidiary.name.toLowerCase().includes(query.name.toLowerCase()), + ), + ); + }} + onReset={onReset} + /> + + + ); +}; + +export default SubsidiarySearch; \ No newline at end of file diff --git a/src/components/SubsidiarySearch/SubsidiarySearchLoading.tsx b/src/components/SubsidiarySearch/SubsidiarySearchLoading.tsx new file mode 100644 index 0000000..b3eb132 --- /dev/null +++ b/src/components/SubsidiarySearch/SubsidiarySearchLoading.tsx @@ -0,0 +1,38 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const SubsidiarySearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SubsidiarySearchLoading; \ No newline at end of file diff --git a/src/components/SubsidiarySearch/SubsidiarySearchWrapper.tsx b/src/components/SubsidiarySearch/SubsidiarySearchWrapper.tsx new file mode 100644 index 0000000..13e8312 --- /dev/null +++ b/src/components/SubsidiarySearch/SubsidiarySearchWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import SubsidiarySearch from "./SubsidiarySearch"; +import SubsidiarySearchLoading from "./SubsidiarySearchLoading"; +import { fetchAllSubsidiaries } from "@/app/api/subsidiary"; + +interface SubComponents { + Loading: typeof SubsidiarySearchLoading; +} + +const SubsidiarySearchWrapper: React.FC & SubComponents = async () => { + const subsidiaries = await fetchAllSubsidiaries(); + + return ; +}; + +SubsidiarySearchWrapper.Loading = SubsidiarySearchLoading; + +export default SubsidiarySearchWrapper; \ No newline at end of file diff --git a/src/components/SubsidiarySearch/index.ts b/src/components/SubsidiarySearch/index.ts new file mode 100644 index 0000000..b09e3a1 --- /dev/null +++ b/src/components/SubsidiarySearch/index.ts @@ -0,0 +1 @@ +export { default } from "./SubsidiarySearchWrapper"; \ No newline at end of file diff --git a/src/i18n/en/customer.json b/src/i18n/en/customer.json index 06d7a6e..38871c6 100644 --- a/src/i18n/en/customer.json +++ b/src/i18n/en/customer.json @@ -35,6 +35,7 @@ "Subsidiary Br No.": "Subsidiary Br No.", "Subsidiary Details": "Subsidiary Details", "Subsidiary Info": "Subsidiary Info", + "Subsidiary Type": "Subsidiary Type", "Add Contact Person": "Add Contact Person", "Contact Details": "Contact Details", @@ -60,5 +61,6 @@ "Confirm": "Confirm", "Submit": "Submit", "Reset": "Reset", - "Delete": "Delete" + "Delete": "Delete", + "Remove": "Remove" } \ No newline at end of file diff --git a/src/i18n/en/subsidiary.json b/src/i18n/en/subsidiary.json new file mode 100644 index 0000000..d26d4e1 --- /dev/null +++ b/src/i18n/en/subsidiary.json @@ -0,0 +1,65 @@ +{ + "Customer": "Client", + "Create Subsidiary": "Create Subsidiary", + "Edit Subsidiary": "Edit Subsidiary", + "Customer Code": "Client Code", + "Customer Name": "Client Name", + "Customer Address": "Client Address", + "Customer District": "Client District", + "Customer Email": "Client Email", + "Customer Phone": "Client Phone", + "Customer Contact Name": "Client Contact Name", + "Customer Br No.": "Client Br No.", + "Customer Details": "Client Details", + "Customer Info": "Client Info", + "Customer Type": "Client Type", + "Customer Allocation": "Client Allocation", + "Search by customer code, name or br no.": "Search by client code, name or br no.", + "Client Pool": "Client Pool", + "Allocated Client": "Allocated Client", + + "Please input correct subsidiary code": "Please input correct client code", + "Please input correct subsidiary name": "Please input correct client name", + "Please input correct subsidiary email": "Please input correct client email", + "Please input correct subsidiary br no.": "Please input correct client br no.", + "The subsidiary code has already existed": "The subsidiary code has already existed", + + "Subsidiary" : "Subsidiary", + "Subsidiary Code": "Subsidiary Code", + "Subsidiary Name": "Subsidiary Name", + "Subsidiary Address": "Subsidiary Address", + "Subsidiary District": "Subsidiary District", + "Subsidiary Email": "Subsidiary Email", + "Subsidiary Phone": "Subsidiary Phone", + "Subsidiary Contact Name": "Subsidiary Contact Name", + "Subsidiary Br No.": "Subsidiary Br No.", + "Subsidiary Details": "Subsidiary Details", + "Subsidiary Info": "Subsidiary Info", + "Subsidiary Type": "Subsidiary Type", + + "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", + "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", + "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", + "Delete": "Delete" +} \ No newline at end of file diff --git a/src/i18n/zh/customer.json b/src/i18n/zh/customer.json index 6eb4e0d..71cb293 100644 --- a/src/i18n/zh/customer.json +++ b/src/i18n/zh/customer.json @@ -35,6 +35,7 @@ "Subsidiary Br No.": "子公司商業登記號碼", "Subsidiary Details": "子公司詳請", "Subsidiary Info": "子公司資料", + "Subsidiary Type": "子公司類型", "Add Contact Person": "新增聯絡人", "Contact Details": "聯絡詳請", @@ -60,5 +61,6 @@ "Confirm": "確認", "Submit": "提交", "Reset": "重置", - "Delete": "刪除" + "Delete": "刪除", + "Remove": "移除" } \ No newline at end of file diff --git a/src/i18n/zh/subsidiary.json b/src/i18n/zh/subsidiary.json new file mode 100644 index 0000000..e1c861a --- /dev/null +++ b/src/i18n/zh/subsidiary.json @@ -0,0 +1,65 @@ +{ + "Customer": "客戶", + "Create Subsidiary": "建立子公司", + "Edit Subsidiary": "編輯子公司", + "Customer Code": "客戶編號", + "Customer Name": "客戶名稱", + "Customer Address": "客戶地址", + "Customer District": "客戶地區", + "Customer Email": "客戶電郵", + "Customer Phone": "客戶電話", + "Customer Contact Name": "客戶聯絡名稱", + "Customer Br No.": "客戶商業登記號碼", + "Customer Details": "客戶詳請", + "Customer Info": "客戶資料", + "Customer Type": "客戶類型", + "Customer Allocation": "客戶分配", + "Search by customer code, name or br no.": "可使用關鍵字搜尋 (客戶編號, 名稱或商業登記號碼)", + "Customer Pool": "所有客戶", + "Allocated Customer": "已分配的客戶", + + "Please input correct subsidiary code": "請輸入子公司編號", + "Please input correct subsidiary name": "請輸入子公司編號", + "Please input correct subsidiary email": "請輸入正確子公司電郵", + "Please input correct subsidiary br no.": "請輸入正確子公司商業登記號碼", + "The subsidiary code has already existed": "該子公司編號已存在", + + "Subsidiary": "子公司", + "Subsidiary Code": "子公司編號", + "Subsidiary Name": "子公司名稱", + "Subsidiary Address": "子公司地址", + "Subsidiary District": "子公司地區", + "Subsidiary Email": "子公司電郵", + "Subsidiary Phone": "子公司電話", + "Subsidiary Contact Name": "子公司聯絡名稱", + "Subsidiary Br No.": "子公司商業登記號碼", + "Subsidiary Details": "子公司詳請", + "Subsidiary Info": "子公司資料", + "Subsidiary Type": "子公司類型", + + "Add Contact Person": "新增聯絡人", + "Contact Details": "聯絡詳請", + "Contact Info": "聯絡資料", + "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": "提交成功", + "Submit Fail": "提交失敗", + "Do you want to delete?": "你是否確認要刪除?", + "Delete Success": "刪除成功", + + "Add": "新增", + "Details": "詳請", + "Info": "資料", + "Search": "搜尋", + "Search Criteria": "搜尋條件", + "Cancel": "取消", + "Confirm": "確認", + "Submit": "提交", + "Reset": "重置", + "Delete": "刪除" +} \ No newline at end of file