Przeglądaj źródła

Update customer master & Add subsidiary master

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 rok temu
rodzic
commit
6a0a46a37c
29 zmienionych plików z 1522 dodań i 39 usunięć
  1. +0
    -2
      src/app/(main)/settings/customer/create/page.tsx
  2. +2
    -2
      src/app/(main)/settings/customer/edit/page.tsx
  3. +23
    -0
      src/app/(main)/settings/subsidiary/create/page.tsx
  4. +23
    -0
      src/app/(main)/settings/subsidiary/edit/page.tsx
  5. +50
    -0
      src/app/(main)/settings/subsidiary/page.tsx
  6. +21
    -1
      src/app/api/customer/index.ts
  7. +74
    -0
      src/app/api/subsidiary/actions.ts
  8. +83
    -0
      src/app/api/subsidiary/index.ts
  9. +0
    -7
      src/app/utils/commonUtil.ts
  10. +3
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  11. +2
    -4
      src/components/CustomerDetail/ContactInfo.tsx
  12. +8
    -8
      src/components/CustomerDetail/CustomerDetail.tsx
  13. +2
    -3
      src/components/CustomerDetail/CustomerDetailWrapper.tsx
  14. +12
    -10
      src/components/CustomerDetail/SubsidiaryAllocation.tsx
  15. +3
    -0
      src/components/NavigationContent/NavigationContent.tsx
  16. +302
    -0
      src/components/SubsidiaryDetail/ContactInfo.tsx
  17. +212
    -0
      src/components/SubsidiaryDetail/CustomerAllocation.tsx
  18. +256
    -0
      src/components/SubsidiaryDetail/SubsidiaryDetail.tsx
  19. +16
    -0
      src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx
  20. +141
    -0
      src/components/SubsidiaryDetail/SubsidiaryInfo.tsx
  21. +1
    -0
      src/components/SubsidiaryDetail/index.ts
  22. +95
    -0
      src/components/SubsidiarySearch/SubsidiarySearch.tsx
  23. +38
    -0
      src/components/SubsidiarySearch/SubsidiarySearchLoading.tsx
  24. +18
    -0
      src/components/SubsidiarySearch/SubsidiarySearchWrapper.tsx
  25. +1
    -0
      src/components/SubsidiarySearch/index.ts
  26. +3
    -1
      src/i18n/en/customer.json
  27. +65
    -0
      src/i18n/en/subsidiary.json
  28. +3
    -1
      src/i18n/zh/customer.json
  29. +65
    -0
      src/i18n/zh/subsidiary.json

+ 0
- 2
src/app/(main)/settings/customer/create/page.tsx Wyświetl plik

@@ -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 (
<>


+ 2
- 2
src/app/(main)/settings/customer/edit/page.tsx Wyświetl plik

@@ -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 (
<>


+ 23
- 0
src/app/(main)/settings/subsidiary/create/page.tsx Wyświetl plik

@@ -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;

+ 23
- 0
src/app/(main)/settings/subsidiary/edit/page.tsx Wyświetl plik

@@ -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;

+ 50
- 0
src/app/(main)/settings/subsidiary/page.tsx Wyświetl plik

@@ -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;

+ 21
- 1
src/app/api/customer/index.ts Wyświetl plik

@@ -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<Customer[]>(`${BASE_API_URL}/customer`);
});

export const fetchSubsidiaries = cache(async () => {
export const fetchAllSubsidiaries = cache(async () => {
return serverFetchJson<Subsidiary[]>(`${BASE_API_URL}/subsidiary`, {
next: { tags: ["subsidiary"] },
});


+ 74
- 0
src/app/api/subsidiary/actions.ts Wyświetl plik

@@ -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
};

+ 83
- 0
src/app/api/subsidiary/index.ts Wyświetl plik

@@ -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"] },
},
);
});

+ 0
- 7
src/app/utils/commonUtil.ts Wyświetl plik

@@ -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));
}

+ 3
- 0
src/components/Breadcrumb/Breadcrumb.tsx Wyświetl plik

@@ -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",


+ 2
- 4
src/components/CustomerDetail/ContactInfo.tsx Wyświetl plik

@@ -88,7 +88,6 @@ const ContactInfo: React.FC<Props> = ({

useEffect(() => {
if (initialRows.length > 0 && rows.length === 0) {
console.log("first")
setRows(initialRows)
}
}, [initialRows.length > 0])
@@ -220,7 +219,7 @@ const ContactInfo: React.FC<Props> = ({

// 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<Props> = ({
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<Props> = ({
}
}
}
}, [rows])
}, [rows, rowModesModel])

// check editing
useEffect(() => {


+ 8
- 8
src/components/CustomerDetail/CustomerDetail.tsx Wyświetl plik

@@ -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<Props> = ({
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<Props> = ({

if (data.addContacts.length === 0 || data.addContacts.filter(row => String(row.name).trim().length === 0 || String(row.phone).trim().length === 0 || String(row.email).trim().length === 0).length > 0) {
haveError = true
formProps.setError("addContacts", { message: "Contact 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<Props> = ({
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("");



+ 2
- 3
src/components/CustomerDetail/CustomerDetailWrapper.tsx Wyświetl plik

@@ -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(),
]);



+ 12
- 10
src/components/CustomerDetail/SubsidiaryAllocation.tsx Wyświetl plik

@@ -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<Props> = ({
const { t } = useTranslation();
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<
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<Props> = ({
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<Props> = ({
);
}, [selectedSubsidiary, setValue]);

const subsidiaryPoolColumns = React.useMemo<Column<Subsidiary>[]>(
const subsidiaryPoolColumns = React.useMemo<Column<SubsidiaryTable>[]>(
() => [
{
label: t("Add"),
@@ -88,11 +88,12 @@ const SubsidiaryAllocation: React.FC<Props> = ({
{ 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<Column<Subsidiary>[]>(
const allocatedSubsidiaryColumns = React.useMemo<Column<SubsidiaryTable>[]>(
() => [
{
label: t("Remove"),
@@ -108,6 +109,7 @@ const SubsidiaryAllocation: React.FC<Props> = ({
{ 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<Props> = ({

React.useEffect(() => {
setFilteredSubsidiary(
subsidiaries.filter((subsidiary) => {
initialSubsidiaries.filter((subsidiary) => {
const q = query.toLowerCase();
return (
(subsidiary.name.toLowerCase().includes(q) ||


+ 3
- 0
src/components/NavigationContent/NavigationContent.tsx Wyświetl plik

@@ -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: <Settings />, label: "Setting", path: "",
children: [
{ icon: <GroupIcon />, label: "Customer", path: "/settings/customer" },
{ icon: <BusinessIcon />, label: "Subsidiary", path: "/settings/subsidiary" },
{ icon: <Staff />, label: "Staff", path: "/settings/staff" },
{ icon: <Company />, label: "Company", path: "/settings/company" },
{ icon: <Department />, label: "Department", path: "/settings/department" },


+ 302
- 0
src/components/SubsidiaryDetail/ContactInfo.tsx Wyświetl plik

@@ -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;

+ 212
- 0
src/components/SubsidiaryDetail/CustomerAllocation.tsx Wyświetl plik

@@ -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;

+ 256
- 0
src/components/SubsidiaryDetail/SubsidiaryDetail.tsx Wyświetl plik

@@ -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;

+ 16
- 0
src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx Wyświetl plik

@@ -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;

+ 141
- 0
src/components/SubsidiaryDetail/SubsidiaryInfo.tsx Wyświetl plik

@@ -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;

+ 1
- 0
src/components/SubsidiaryDetail/index.ts Wyświetl plik

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

+ 95
- 0
src/components/SubsidiarySearch/SubsidiarySearch.tsx Wyświetl plik

@@ -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;

+ 38
- 0
src/components/SubsidiarySearch/SubsidiarySearchLoading.tsx Wyświetl plik

@@ -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;

+ 18
- 0
src/components/SubsidiarySearch/SubsidiarySearchWrapper.tsx Wyświetl plik

@@ -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;

+ 1
- 0
src/components/SubsidiarySearch/index.ts Wyświetl plik

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

+ 3
- 1
src/i18n/en/customer.json Wyświetl plik

@@ -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"
}

+ 65
- 0
src/i18n/en/subsidiary.json Wyświetl plik

@@ -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"
}

+ 3
- 1
src/i18n/zh/customer.json Wyświetl plik

@@ -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": "移除"
}

+ 65
- 0
src/i18n/zh/subsidiary.json Wyświetl plik

@@ -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": "刪除"
}

Ładowanie…
Anuluj
Zapisz