| @@ -1,4 +1,3 @@ | |||||
| import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; | |||||
| import CustomerDetail from "@/components/CustomerDetail"; | import CustomerDetail from "@/components/CustomerDetail"; | ||||
| // import { preloadAllTasks } from "@/app/api/tasks"; | // import { preloadAllTasks } from "@/app/api/tasks"; | ||||
| import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | ||||
| @@ -12,7 +11,6 @@ export const metadata: Metadata = { | |||||
| const CreateCustomer: React.FC = async () => { | const CreateCustomer: React.FC = async () => { | ||||
| const { t } = await getServerI18n("customer"); | const { t } = await getServerI18n("customer"); | ||||
| // fetchSubsidiaries(); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -1,4 +1,4 @@ | |||||
| import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; | |||||
| import { fetchAllSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; | |||||
| import CustomerDetail from "@/components/CustomerDetail"; | import CustomerDetail from "@/components/CustomerDetail"; | ||||
| // import { preloadAllTasks } from "@/app/api/tasks"; | // import { preloadAllTasks } from "@/app/api/tasks"; | ||||
| import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | ||||
| @@ -12,7 +12,7 @@ export const metadata: Metadata = { | |||||
| const EditCustomer: React.FC = async () => { | const EditCustomer: React.FC = async () => { | ||||
| const { t } = await getServerI18n("customer"); | const { t } = await getServerI18n("customer"); | ||||
| // fetchSubsidiaries(); | |||||
| // fetchAllSubsidiaries(); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -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 ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Create Subsidiary")}</Typography> | |||||
| <I18nProvider namespaces={["subsidiary", "common"]}> | |||||
| <SubsidiaryDetail /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateSubsidiary; | |||||
| @@ -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 ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Edit Subsidiary")}</Typography> | |||||
| <I18nProvider namespaces={["subsidiary", "common"]}> | |||||
| <SubsidiaryDetail /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default EditSubsidiary; | |||||
| @@ -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 ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Subsidiary")} | |||||
| </Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/settings/subsidiary/create" | |||||
| > | |||||
| {t("Create Subsidiary")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <I18nProvider namespaces={["subsidiary", "common"]}> | |||||
| <Suspense fallback={<SubsidiarySearch.Loading />}> | |||||
| <SubsidiarySearch /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Subsidiary; | |||||
| @@ -23,6 +23,11 @@ export interface CustomerType { | |||||
| name: string; | name: string; | ||||
| } | } | ||||
| export interface SubsidiaryType { | |||||
| id: number; | |||||
| name: string; | |||||
| } | |||||
| export interface Subsidiary { | export interface Subsidiary { | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| @@ -34,6 +39,21 @@ export interface Subsidiary { | |||||
| address: string | null; | address: string | null; | ||||
| district: string | null; | district: string | null; | ||||
| email: 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 { | export interface Contact { | ||||
| @@ -52,7 +72,7 @@ export const fetchAllCustomers = cache(async () => { | |||||
| return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`); | return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`); | ||||
| }); | }); | ||||
| export const fetchSubsidiaries = cache(async () => { | |||||
| export const fetchAllSubsidiaries = cache(async () => { | |||||
| return serverFetchJson<Subsidiary[]>(`${BASE_API_URL}/subsidiary`, { | return serverFetchJson<Subsidiary[]>(`${BASE_API_URL}/subsidiary`, { | ||||
| next: { tags: ["subsidiary"] }, | next: { tags: ["subsidiary"] }, | ||||
| }); | }); | ||||
| @@ -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<SaveSubsidiaryResponse>( | |||||
| `${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<SubsidiaryResponse>( | |||||
| `${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 | |||||
| }; | |||||
| @@ -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<Customer[]>(`${BASE_API_URL}/customer`); | |||||
| }); | |||||
| export const fetchAllSubsidiaries = cache(async () => { | |||||
| return serverFetchJson<Subsidiary[]>( | |||||
| `${BASE_API_URL}/subsidiary`, | |||||
| { | |||||
| next: { tags: ["subsidiary"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const fetchSubsidiaryTypes = cache(async () => { | |||||
| return serverFetchJson<SubsidiaryType[]>( | |||||
| `${BASE_API_URL}/subsidiary/types`, | |||||
| { | |||||
| next: { tags: ["subsidiaryTypes"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| @@ -1,7 +0,0 @@ | |||||
| 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>) { | |||||
| return updatedList.filter(x => !referenceList.includes(x)); | |||||
| } | |||||
| @@ -17,6 +17,9 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
| "/settings/customer": "Customer", | "/settings/customer": "Customer", | ||||
| "/settings/customer/create": "Create Customer", | "/settings/customer/create": "Create Customer", | ||||
| "/settings/customer/edit": "Edit Customer", | "/settings/customer/edit": "Edit Customer", | ||||
| "/settings/subsidiary": "Subsidiary", | |||||
| "/settings/subsidiary/create": "Create Subsidiary", | |||||
| "/settings/subsidiary/edit": "Edit Subsidiary", | |||||
| "/settings": "Settings", | "/settings": "Settings", | ||||
| "/company": "Company", | "/company": "Company", | ||||
| "/settings/department": "Department", | "/settings/department": "Department", | ||||
| @@ -88,7 +88,6 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (initialRows.length > 0 && rows.length === 0) { | if (initialRows.length > 0 && rows.length === 0) { | ||||
| console.log("first") | |||||
| setRows(initialRows) | setRows(initialRows) | ||||
| } | } | ||||
| }, [initialRows.length > 0]) | }, [initialRows.length > 0]) | ||||
| @@ -220,7 +219,7 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| // check error | // check error | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (getValues("addContacts") !== undefined || getValues("addContacts") !== null) { | |||||
| if (getValues("addContacts") === undefined || getValues("addContacts") === null) { | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -233,7 +232,6 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | ||||
| } else { | } else { | ||||
| const errorRows_EmailFormat = rows.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))) | 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) { | if (errorRows_EmailFormat.length > 0) { | ||||
| setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | ||||
| } else { | } else { | ||||
| @@ -241,7 +239,7 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| }, [rows]) | |||||
| }, [rows, rowModesModel]) | |||||
| // check editing | // check editing | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -22,8 +22,8 @@ import { CustomerFormInputs, fetchCustomer, saveCustomer } from "@/app/api/custo | |||||
| import CustomerInfo from "./CustomerInfo"; | import CustomerInfo from "./CustomerInfo"; | ||||
| import SubsidiaryAllocation from "./SubsidiaryAllocation"; | import SubsidiaryAllocation from "./SubsidiaryAllocation"; | ||||
| import { CustomerType, Subsidiary } from "@/app/api/customer"; | import { CustomerType, Subsidiary } from "@/app/api/customer"; | ||||
| import { getDeletedRecordWithRefList } from "@/app/utils/commonUtil"; | |||||
| import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; | import { errorDialog, submitDialog, successDialog, warningDialog } from "../Swal/CustomAlerts"; | ||||
| import { differenceBy } from "lodash"; | |||||
| export interface Props { | export interface Props { | ||||
| subsidiaries: Subsidiary[], | subsidiaries: Subsidiary[], | ||||
| @@ -80,9 +80,9 @@ const CustomerDetail: React.FC<Props> = ({ | |||||
| id: customer.customer.id, | id: customer.customer.id, | ||||
| code: customer.customer.code ?? "", | code: customer.customer.code ?? "", | ||||
| name: customer.customer.name ?? "", | 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, | typeId: customer.customer.customerType.id, | ||||
| addContacts: customer.contacts ?? [], | addContacts: customer.contacts ?? [], | ||||
| addSubsidiaryIds: customer.subsidiaryIds ?? [], | addSubsidiaryIds: customer.subsidiaryIds ?? [], | ||||
| @@ -177,12 +177,12 @@ const CustomerDetail: 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) { | 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 | 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) { | 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 | 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) { | if (haveError) { | ||||
| @@ -191,8 +191,8 @@ const CustomerDetail: React.FC<Props> = ({ | |||||
| return false | 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(""); | setServerError(""); | ||||
| @@ -2,9 +2,8 @@ | |||||
| // 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 { fetchCustomerTypes, fetchSubsidiaries } from "@/app/api/customer"; | |||||
| import { Subsidiary, fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import CustomerDetail from "./CustomerDetail"; | import CustomerDetail from "./CustomerDetail"; | ||||
| import { getServerSideProps } from "next/dist/build/templates/pages"; | |||||
| // type Props = { | // type Props = { | ||||
| // params: { | // params: { | ||||
| @@ -17,7 +16,7 @@ const CustomerDetailWrapper: React.FC = async () => { | |||||
| // console.log(params) | // console.log(params) | ||||
| const [subsidiaries, customerTypes] = | const [subsidiaries, customerTypes] = | ||||
| await Promise.all([ | await Promise.all([ | ||||
| fetchSubsidiaries(), | |||||
| fetchAllSubsidiaries(), | |||||
| fetchCustomerTypes(), | fetchCustomerTypes(), | ||||
| ]); | ]); | ||||
| @@ -24,7 +24,7 @@ import { | |||||
| import differenceBy from "lodash/differenceBy"; | import differenceBy from "lodash/differenceBy"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { CustomerFormInputs } from "@/app/api/customer/actions"; | import { CustomerFormInputs } from "@/app/api/customer/actions"; | ||||
| import { Subsidiary } from "@/app/api/customer"; | |||||
| import { Subsidiary, SubsidiaryTable } from "@/app/api/customer"; | |||||
| interface Props { | interface Props { | ||||
| subsidiaries: Subsidiary[]; | subsidiaries: Subsidiary[]; | ||||
| @@ -36,21 +36,21 @@ const SubsidiaryAllocation: React.FC<Props> = ({ | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { setValue, getValues, formState: { defaultValues }, reset, resetField } = useFormContext<CustomerFormInputs>(); | const { setValue, getValues, formState: { defaultValues }, reset, resetField } = useFormContext<CustomerFormInputs>(); | ||||
| 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< | const [selectedSubsidiary, setSelectedSubsidiary] = React.useState< | ||||
| typeof filteredSubsidiary | typeof filteredSubsidiary | ||||
| >( | |||||
| subsidiaries.filter((subsidiary) => | |||||
| >(initialSubsidiaries.filter((subsidiary) => | |||||
| getValues("addSubsidiaryIds")?.includes(subsidiary.id), | getValues("addSubsidiaryIds")?.includes(subsidiary.id), | ||||
| ) | ) | ||||
| ); | ); | ||||
| // Adding / Removing staff | // Adding / Removing staff | ||||
| const addSubsidiary = React.useCallback((subsidiary: Subsidiary) => { | |||||
| const addSubsidiary = React.useCallback((subsidiary: SubsidiaryTable) => { | |||||
| setSelectedSubsidiary((subsidiaries) => [...subsidiaries, subsidiary]); | 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)); | setSelectedSubsidiary((subsidiaries) => subsidiaries.filter((s) => s.id !== subsidiary.id)); | ||||
| }, []); | }, []); | ||||
| @@ -58,7 +58,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({ | |||||
| if (defaultValues !== undefined) { | if (defaultValues !== undefined) { | ||||
| // reset({ addSubsidiaryIds: defaultValues.addSubsidiaryIds }) | // reset({ addSubsidiaryIds: defaultValues.addSubsidiaryIds }) | ||||
| resetField("addSubsidiaryIds") | resetField("addSubsidiaryIds") | ||||
| setSelectedSubsidiary(subsidiaries.filter((subsidiary) => | |||||
| setSelectedSubsidiary(initialSubsidiaries.filter((subsidiary) => | |||||
| defaultValues.addSubsidiaryIds?.includes(subsidiary.id), | defaultValues.addSubsidiaryIds?.includes(subsidiary.id), | ||||
| )) | )) | ||||
| } | } | ||||
| @@ -72,7 +72,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({ | |||||
| ); | ); | ||||
| }, [selectedSubsidiary, setValue]); | }, [selectedSubsidiary, setValue]); | ||||
| const subsidiaryPoolColumns = React.useMemo<Column<Subsidiary>[]>( | |||||
| const subsidiaryPoolColumns = React.useMemo<Column<SubsidiaryTable>[]>( | |||||
| () => [ | () => [ | ||||
| { | { | ||||
| label: t("Add"), | label: t("Add"), | ||||
| @@ -88,11 +88,12 @@ const SubsidiaryAllocation: React.FC<Props> = ({ | |||||
| { label: t("Subsidiary Address"), name: "address" }, | { label: t("Subsidiary Address"), name: "address" }, | ||||
| { label: t("Subsidiary District"), name: "district" }, | { label: t("Subsidiary District"), name: "district" }, | ||||
| // { label: t("Subsidiary Email"), name: "email" }, | // { label: t("Subsidiary Email"), name: "email" }, | ||||
| { label: t("Subsidiary Type"), name: "subsidiaryType" }, | |||||
| ], | ], | ||||
| [addSubsidiary, t], | [addSubsidiary, t], | ||||
| ); | ); | ||||
| const allocatedSubsidiaryColumns = React.useMemo<Column<Subsidiary>[]>( | |||||
| const allocatedSubsidiaryColumns = React.useMemo<Column<SubsidiaryTable>[]>( | |||||
| () => [ | () => [ | ||||
| { | { | ||||
| label: t("Remove"), | label: t("Remove"), | ||||
| @@ -108,6 +109,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({ | |||||
| { label: t("Subsidiary Address"), name: "address" }, | { label: t("Subsidiary Address"), name: "address" }, | ||||
| { label: t("Subsidiary District"), name: "district" }, | { label: t("Subsidiary District"), name: "district" }, | ||||
| // { label: t("Subsidiary Email"), name: "email" }, | // { label: t("Subsidiary Email"), name: "email" }, | ||||
| { label: t("Subsidiary Type"), name: "subsidiaryType" }, | |||||
| ], | ], | ||||
| [removeSubsidiary, t], | [removeSubsidiary, t], | ||||
| ); | ); | ||||
| @@ -125,7 +127,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({ | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| setFilteredSubsidiary( | setFilteredSubsidiary( | ||||
| subsidiaries.filter((subsidiary) => { | |||||
| initialSubsidiaries.filter((subsidiary) => { | |||||
| const q = query.toLowerCase(); | const q = query.toLowerCase(); | ||||
| return ( | return ( | ||||
| (subsidiary.name.toLowerCase().includes(q) || | (subsidiary.name.toLowerCase().includes(q) || | ||||
| @@ -29,6 +29,8 @@ import Link from "next/link"; | |||||
| import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | ||||
| import Logo from "../Logo"; | import Logo from "../Logo"; | ||||
| import GroupIcon from '@mui/icons-material/Group'; | import GroupIcon from '@mui/icons-material/Group'; | ||||
| import BusinessIcon from '@mui/icons-material/Business'; | |||||
| interface NavigationItem { | interface NavigationItem { | ||||
| icon: React.ReactNode; | icon: React.ReactNode; | ||||
| label: string; | label: string; | ||||
| @@ -100,6 +102,7 @@ const navigationItems: NavigationItem[] = [ | |||||
| icon: <Settings />, label: "Setting", path: "", | icon: <Settings />, label: "Setting", path: "", | ||||
| children: [ | children: [ | ||||
| { icon: <GroupIcon />, label: "Customer", path: "/settings/customer" }, | { icon: <GroupIcon />, label: "Customer", path: "/settings/customer" }, | ||||
| { icon: <BusinessIcon />, label: "Subsidiary", path: "/settings/subsidiary" }, | |||||
| { icon: <Staff />, label: "Staff", path: "/settings/staff" }, | { 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" }, | ||||
| @@ -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 ( | |||||
| <GridToolbarContainer> | |||||
| <Button color="primary" startIcon={<AddIcon />} onClick={handleClick}> | |||||
| {t("Add Contact Person")} | |||||
| </Button> | |||||
| </GridToolbarContainer> | |||||
| ); | |||||
| } | |||||
| const ContactInfo: React.FC<Props> = ({ | |||||
| }) => { | |||||
| 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<GridRowsProp>([]); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
| 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<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") === 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 ( | |||||
| <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 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")} | |||||
| </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 />} onClick={resetContact} disabled={Boolean(watch("isGridEditing"))}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default ContactInfo; | |||||
| @@ -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<Props> = ({ | |||||
| customers, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { setValue, getValues, formState: { defaultValues }, reset, resetField } = useFormContext<SubsidiaryFormInputs>(); | |||||
| 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<Column<CustomerTable>[]>( | |||||
| () => [ | |||||
| { | |||||
| label: t("Add"), | |||||
| name: "id", | |||||
| onClick: addCustomer, | |||||
| buttonIcon: <PersonAdd />, | |||||
| }, | |||||
| { 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<Column<CustomerTable>[]>( | |||||
| () => [ | |||||
| { | |||||
| label: t("Remove"), | |||||
| name: "id", | |||||
| onClick: removeCustomer, | |||||
| buttonIcon: <PersonRemove />, | |||||
| }, | |||||
| { 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<HTMLInputElement> | |||||
| >((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<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const resetCustomer = React.useCallback(() => { | |||||
| clearQueryInput(); | |||||
| clearCustomer(); | |||||
| }, [clearQueryInput, clearCustomer]); | |||||
| return ( | |||||
| <> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <Stack gap={2}> | |||||
| <Typography variant="overline" display="block"> | |||||
| {t("Customer Allocation")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6} display="flex" alignItems="center"> | |||||
| <Search sx={{ marginInlineEnd: 1 }} /> | |||||
| <TextField | |||||
| variant="standard" | |||||
| fullWidth | |||||
| onChange={onQueryInputChange} | |||||
| value={query} | |||||
| placeholder={t("Search by customer code, name or br no.")} | |||||
| InputProps={{ | |||||
| endAdornment: query && ( | |||||
| <InputAdornment position="end"> | |||||
| <IconButton onClick={clearQueryInput}> | |||||
| <Clear /> | |||||
| </IconButton> | |||||
| </InputAdornment> | |||||
| ), | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| <Tabs value={tabIndex} onChange={handleTabChange}> | |||||
| <Tab label={t("Customer Pool")} /> | |||||
| <Tab | |||||
| label={`${t("Allocated Customer")} (${selectedCustomer.length})`} | |||||
| /> | |||||
| </Tabs> | |||||
| <Box sx={{ marginInline: -3 }}> | |||||
| {tabIndex === 0 && ( | |||||
| <SearchResults | |||||
| noWrapper | |||||
| items={differenceBy(filteredCustomer, selectedCustomer, "id")} | |||||
| columns={customerPoolColumns} | |||||
| /> | |||||
| )} | |||||
| {tabIndex === 1 && ( | |||||
| <SearchResults | |||||
| noWrapper | |||||
| items={selectedCustomer} | |||||
| columns={allocatedCustomerColumns} | |||||
| /> | |||||
| )} | |||||
| </Box> | |||||
| </Stack> | |||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button variant="text" startIcon={<RestartAlt />} onClick={resetCustomer}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CustomerAllocation; | |||||
| @@ -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<SubsidiaryFormInputs>, | |||||
| ) => { | |||||
| switch (tabIndex) { | |||||
| case 0: | |||||
| return Object.keys(errors).length > 0; | |||||
| default: | |||||
| false; | |||||
| } | |||||
| }; | |||||
| const SubsidiaryDetail: React.FC<Props> = ({ | |||||
| customers, | |||||
| subsidiaryTypes, | |||||
| }) => { | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const [refSubsidiary, setRefSubsidiary] = useState<SubsidiaryFormInputs>() | |||||
| 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<SubsidiaryFormInputs>(); | |||||
| useEffect(() => { | |||||
| if (refSubsidiary !== null && refSubsidiary !== undefined) { | |||||
| formProps.reset(refSubsidiary) | |||||
| } | |||||
| }, [refSubsidiary]) | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const onSubmit = useCallback<SubmitHandler<SubsidiaryFormInputs>>( | |||||
| 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<SubmitErrorHandler<SubsidiaryFormInputs>>( | |||||
| (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 ( | |||||
| <FormProvider {...formProps}> | |||||
| {refSubsidiary && <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | |||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||||
| <Tab | |||||
| label={t("Subsidiary Info")} | |||||
| icon={ | |||||
| hasErrorsInTab(0, errors) ? ( | |||||
| <Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
| ) : undefined | |||||
| } | |||||
| iconPosition="end" | |||||
| /> | |||||
| <Tab label={t("Customer Allocation")} iconPosition="end" /> | |||||
| </Tabs> | |||||
| {serverError && ( | |||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
| {serverError} | |||||
| </Typography> | |||||
| )} | |||||
| {tabIndex === 0 && <SubsidiaryInfo subsidiaryTypes={subsidiaryTypes} />} | |||||
| {tabIndex === 1 && <CustomerAllocation customers={customers} />} | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button variant="contained" startIcon={<Check />} type="submit" disabled={Boolean(formProps.watch("isGridEditing"))}> | |||||
| {t("Confirm")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack>} | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default SubsidiaryDetail; | |||||
| @@ -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 ( | |||||
| <SubsidiaryDetail customers={customers} subsidiaryTypes={subsidiaryTypes} /> | |||||
| ); | |||||
| }; | |||||
| export default CustomerDetailWrapper; | |||||
| @@ -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<Props> = ({ | |||||
| subsidiaryTypes, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors, defaultValues }, | |||||
| control, | |||||
| reset, | |||||
| resetField, | |||||
| setValue | |||||
| } = useFormContext<CustomerFormInputs>(); | |||||
| const resetSubsidiary = useCallback(() => { | |||||
| if (defaultValues !== undefined) { | |||||
| resetField("code") | |||||
| resetField("name") | |||||
| resetField("address") | |||||
| resetField("district") | |||||
| resetField("typeId") | |||||
| resetField("brNo") | |||||
| } | |||||
| }, [defaultValues]) | |||||
| return ( | |||||
| <> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Subsidiary Info")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Subsidiary 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 Subsidiary code"))} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Subsidiary Name")} | |||||
| fullWidth | |||||
| {...register("name", { | |||||
| required: true, | |||||
| })} | |||||
| error={Boolean(errors.name)} | |||||
| helperText={Boolean(errors.name) && t("Please input correct Subsidiary name")} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Subsidiary Address")} | |||||
| fullWidth | |||||
| {...register("address")} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Subsidiary District")} | |||||
| fullWidth | |||||
| {...register("district")} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("Subsidiary Type")}</InputLabel> | |||||
| <Controller | |||||
| defaultValue={subsidiaryTypes[0].id} | |||||
| control={control} | |||||
| name="typeId" | |||||
| render={({ field }) => ( | |||||
| <Select label={t("Project Category")} {...field}> | |||||
| {subsidiaryTypes.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("Subsidiary Br No.")} | |||||
| fullWidth | |||||
| {...register("brNo", { | |||||
| pattern: /[0-9]{8}/, | |||||
| })} | |||||
| error={Boolean(errors.brNo)} | |||||
| helperText={Boolean(errors.brNo) && t("Please input correct subsidiary br no.")} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button onClick={resetSubsidiary} variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <ContactInfo /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default SubsidiaryInfo; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./SubsidiaryDetailWrapper"; | |||||
| @@ -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<Omit<Subsidiary, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const SubsidiarySearch: React.FC<Props> = ({ subsidiaries }) => { | |||||
| const { t } = useTranslation(); | |||||
| const router = useRouter() | |||||
| const searchParams = useSearchParams() | |||||
| const [filteredSubsidiaries, setFilteredSubsidiaries] = useState(subsidiaries); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = 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<Column<Subsidiary>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "id", | |||||
| label: t("Details"), | |||||
| onClick: onTaskClick, | |||||
| buttonIcon: <EditNote />, | |||||
| }, | |||||
| { name: "code", label: t("Subsidiary Code") }, | |||||
| { name: "name", label: t("Subsidiary Name") }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Delete"), | |||||
| onClick: onDeleteClick, | |||||
| buttonIcon: <DeleteIcon />, | |||||
| }, | |||||
| ], | |||||
| [onTaskClick, t], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| setFilteredSubsidiaries( | |||||
| subsidiaries.filter( | |||||
| (subsidiary) => | |||||
| subsidiary.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| subsidiary.name.toLowerCase().includes(query.name.toLowerCase()), | |||||
| ), | |||||
| ); | |||||
| }} | |||||
| onReset={onReset} | |||||
| /> | |||||
| <SearchResults items={filteredSubsidiaries} columns={columns} /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default SubsidiarySearch; | |||||
| @@ -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 ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default SubsidiarySearchLoading; | |||||
| @@ -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 <SubsidiarySearch subsidiaries={subsidiaries} />; | |||||
| }; | |||||
| SubsidiarySearchWrapper.Loading = SubsidiarySearchLoading; | |||||
| export default SubsidiarySearchWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./SubsidiarySearchWrapper"; | |||||
| @@ -35,6 +35,7 @@ | |||||
| "Subsidiary Br No.": "Subsidiary Br No.", | "Subsidiary Br No.": "Subsidiary Br No.", | ||||
| "Subsidiary Details": "Subsidiary Details", | "Subsidiary Details": "Subsidiary Details", | ||||
| "Subsidiary Info": "Subsidiary Info", | "Subsidiary Info": "Subsidiary Info", | ||||
| "Subsidiary Type": "Subsidiary Type", | |||||
| "Add Contact Person": "Add Contact Person", | "Add Contact Person": "Add Contact Person", | ||||
| "Contact Details": "Contact Details", | "Contact Details": "Contact Details", | ||||
| @@ -60,5 +61,6 @@ | |||||
| "Confirm": "Confirm", | "Confirm": "Confirm", | ||||
| "Submit": "Submit", | "Submit": "Submit", | ||||
| "Reset": "Reset", | "Reset": "Reset", | ||||
| "Delete": "Delete" | |||||
| "Delete": "Delete", | |||||
| "Remove": "Remove" | |||||
| } | } | ||||
| @@ -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" | |||||
| } | |||||
| @@ -35,6 +35,7 @@ | |||||
| "Subsidiary Br No.": "子公司商業登記號碼", | "Subsidiary Br No.": "子公司商業登記號碼", | ||||
| "Subsidiary Details": "子公司詳請", | "Subsidiary Details": "子公司詳請", | ||||
| "Subsidiary Info": "子公司資料", | "Subsidiary Info": "子公司資料", | ||||
| "Subsidiary Type": "子公司類型", | |||||
| "Add Contact Person": "新增聯絡人", | "Add Contact Person": "新增聯絡人", | ||||
| "Contact Details": "聯絡詳請", | "Contact Details": "聯絡詳請", | ||||
| @@ -60,5 +61,6 @@ | |||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Submit": "提交", | "Submit": "提交", | ||||
| "Reset": "重置", | "Reset": "重置", | ||||
| "Delete": "刪除" | |||||
| "Delete": "刪除", | |||||
| "Remove": "移除" | |||||
| } | } | ||||
| @@ -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": "刪除" | |||||
| } | |||||