Преглед изворни кода

add customer master page

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui пре 1 година
родитељ
комит
c957697028
24 измењених фајлова са 963 додато и 22 уклоњено
  1. +27
    -0
      src/app/(main)/customer/create/page.tsx
  2. +50
    -0
      src/app/(main)/customer/page.tsx
  3. +4
    -4
      src/app/(main)/layout.tsx
  4. +38
    -0
      src/app/api/customer/actions.ts
  5. +40
    -0
      src/app/api/customer/index.ts
  6. +7
    -0
      src/app/utils/commonUtil.ts
  7. +6
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  8. +171
    -0
      src/components/CreateCustomer/CreateCustomer.tsx
  9. +19
    -0
      src/components/CreateCustomer/CreateCustomerWrapper.tsx
  10. +120
    -0
      src/components/CreateCustomer/CustomerDetails.tsx
  11. +214
    -0
      src/components/CreateCustomer/SubsidiaryAllocation.tsx
  12. +1
    -0
      src/components/CreateCustomer/index.ts
  13. +70
    -0
      src/components/CustomerSearch/CustomerSearch.tsx
  14. +38
    -0
      src/components/CustomerSearch/CustomerSearchLoading.tsx
  15. +18
    -0
      src/components/CustomerSearch/CustomerSearchWrapper.tsx
  16. +1
    -0
      src/components/CustomerSearch/index.ts
  17. +16
    -16
      src/components/SearchBox/SearchBox.tsx
  18. +13
    -0
      src/components/SearchResults/SearchResults.tsx
  19. +6
    -0
      src/i18n/en/breadcrumb.json
  20. +7
    -1
      src/i18n/en/common.json
  21. +42
    -0
      src/i18n/en/customer.json
  22. +6
    -0
      src/i18n/zh/breadcrumb.json
  23. +7
    -1
      src/i18n/zh/common.json
  24. +42
    -0
      src/i18n/zh/customer.json

+ 27
- 0
src/app/(main)/customer/create/page.tsx Прегледај датотеку

@@ -0,0 +1,27 @@
import { fetchSubsidiaries, preloadAllCustomers } from "@/app/api/customer";
import CreateCustomer from "@/components/CreateCustomer";
// import { preloadAllTasks } from "@/app/api/tasks";
import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";

export const metadata: Metadata = {
title: "Create Customer",
};

const Projects: React.FC = async () => {
const { t } = await getServerI18n("customer");
// fetchSubsidiaries();

return (
<>
<Typography variant="h4">{t("Create Customer")}</Typography>
<I18nProvider namespaces={["customer"]}>
<CreateCustomer />
</I18nProvider>
</>
);
};

export default Projects;

+ 50
- 0
src/app/(main)/customer/page.tsx Прегледај датотеку

@@ -0,0 +1,50 @@
import { preloadAllCustomers } from "@/app/api/customer";
import CustomerSearch from "@/components/CustomerSearch";
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";

export const metadata: Metadata = {
title: "Customer",
};

const Customer: React.FC = async () => {
const { t } = await getServerI18n("customer");
preloadAllCustomers();

return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Customer")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/customer/create"
>
{t("Create Customer")}
</Button>
</Stack>
<I18nProvider namespaces={["customer", "common"]}>
<Suspense fallback={<CustomerSearch.Loading />}>
<CustomerSearch />
</Suspense>
</I18nProvider>
</>
);
};

export default Customer;

+ 4
- 4
src/app/(main)/layout.tsx Прегледај датотеку

@@ -31,10 +31,10 @@ export default async function MainLayout({
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" },
}}
>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
</Box>
</>
);


+ 38
- 0
src/app/api/customer/actions.ts Прегледај датотеку

@@ -0,0 +1,38 @@
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Customer } from ".";
import { revalidateTag } from "next/cache";

export interface CustomerFormInputs {

// Customer details
name: string;
code: string;
address: string | null;
district: string | null;
email: string | null;
phone: string | null;
contactName: string | null;
brNo: string | null;

// Subsidiary
addSubsidiaryIds: number[];
deleteSubsidiaryIds: number[];
}

export const saveCustomer = async (data: CustomerFormInputs) => {
const saveCustomer = await serverFetchJson<Customer>(
`${BASE_API_URL}/customer/save`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

revalidateTag("customers");

return saveCustomer;
};

+ 40
- 0
src/app/api/customer/index.ts Прегледај датотеку

@@ -0,0 +1,40 @@
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;
}

export interface Subsidiary {
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;
}

export const preloadAllCustomers = () => {
fetchAllCustomers();
};

export const fetchAllCustomers = cache(async () => {
return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`);
});

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

+ 7
- 0
src/app/utils/commonUtil.ts Прегледај датотеку

@@ -0,0 +1,7 @@
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));
}

+ 6
- 0
src/components/Breadcrumb/Breadcrumb.tsx Прегледај датотеку

@@ -5,6 +5,7 @@ import Typography from "@mui/material/Typography";
import Link from "next/link";
import MUILink from "@mui/material/Link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";

const pathToLabelMap: { [path: string]: string } = {
"": "Overview",
@@ -12,11 +13,15 @@ const pathToLabelMap: { [path: string]: string } = {
"/projects/create": "Create Project",
"/tasks": "Task Template",
"/tasks/create": "Create Task Template",
"/customer": "Customer",
"/customer/create": "Create Customer",
};

const Breadcrumb = () => {
const pathname = usePathname();
const segments = pathname.split("/");
// const { t } = useTranslation("customer");

return (
<Breadcrumbs>
@@ -28,6 +33,7 @@ const Breadcrumb = () => {
return (
<Typography key={index} color="text.primary">
{label}
{/* {t(label)} */}
</Typography>
);
} else {


+ 171
- 0
src/components/CreateCustomer/CreateCustomer.tsx Прегледај датотеку

@@ -0,0 +1,171 @@
"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 } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Task, TaskTemplate } from "@/app/api/tasks";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import { Staff } from "@/app/api/staff";
import { Typography } from "@mui/material";
import { CustomerFormInputs, saveCustomer } from "@/app/api/customer/actions";
import CustomerDetails from "./CustomerDetails";
import SubsidiaryAllocation from "./SubsidiaryAllocation";
import { Subsidiary } from "@/app/api/customer";
import { getDeletedRecordWithRefList } from "@/app/utils/commonUtil";

export interface Props {
subsidiaries: Subsidiary[],
}

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CustomerFormInputs>,
) => {
switch (tabIndex) {
case 0:
return errors.name;
default:
false;
}
};

const CreateCustomer: React.FC<Props> = ({
subsidiaries,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const router = useRouter();
const formProps = useForm<CustomerFormInputs>({
defaultValues: {
code: "",
name: "",
},
});

const handleCancel = () => {
router.back();
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

const onSubmit = useCallback<SubmitHandler<CustomerFormInputs>>(
async (data) => {
try {
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.email && data.email?.length > 0 && !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(data.email)) {
haveError = true
formProps.setError("email", {message: "Email format is not valid", type: "custom"})
}

if (data.brNo && data.brNo?.length > 0 && !/[0-9]{8}/.test(data.brNo)) {
haveError = true
formProps.setError("brNo", {message: "Br No. format is not valid", type: "custom"})
}

if (haveError) {
// go to the error tab
setTabIndex(0)
return false
}

data.deleteSubsidiaryIds = []
setServerError("");
await saveCustomer(data);
router.replace("/customer");
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t],
);

const onSubmitError = useCallback<SubmitErrorHandler<CustomerFormInputs>>(
(errors) => {
// Set the tab so that the focus will go there
if (errors.name || errors.code) {
setTabIndex(0);
}
},
[],
);

const errors = formProps.formState.errors;

return (
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab
label={t("Customer Details")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("Subsidiary Allocation")} iconPosition="end" />
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <CustomerDetails/>}
{tabIndex === 1 && <SubsidiaryAllocation subsidiaries={subsidiaries}/>}

<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">
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
);
};

export default CreateCustomer;

+ 19
- 0
src/components/CreateCustomer/CreateCustomerWrapper.tsx Прегледај датотеку

@@ -0,0 +1,19 @@
// import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
// import CreateProject from "./CreateProject";
// import { fetchProjectCategories } from "@/app/api/projects";
// import { fetchTeamLeads } from "@/app/api/staff";
import { fetchSubsidiaries } from "@/app/api/customer";
import CreateCustomer from "./CreateCustomer";

const CreateCustomerWrapper: React.FC = async () => {
const [subsidiaries] =
await Promise.all([
fetchSubsidiaries(),
]);

return (
<CreateCustomer subsidiaries={subsidiaries}/>
);
};

export default CreateCustomerWrapper;

+ 120
- 0
src/components/CreateCustomer/CustomerDetails.tsx Прегледај датотеку

@@ -0,0 +1,120 @@
"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 { useFormContext } from "react-hook-form";
import { CustomerFormInputs } from "@/app/api/customer/actions";

interface Props {
}

const CustomerDetails: React.FC<Props> = ({
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
} = useFormContext<CustomerFormInputs>();

return (
<Card sx={{ display: "block"}}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Customer Details")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Customer Code")}
fullWidth
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={Boolean(errors.code) && t("Please input correct customer code")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Name")}
fullWidth
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={Boolean(errors.name) && t("Please input correct customer name")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Address")}
fullWidth
{...register("address")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer District")}
fullWidth
{...register("district")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Email")}
fullWidth
{...register("email", {
pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/,
})}
error={Boolean(errors.email)}
helperText={Boolean(errors.email) && t("Please input correct customer email")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Phone")}
fullWidth
{...register("phone")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Contact Name")}
fullWidth
{...register("contactName")}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Customer Br No.")}
fullWidth
{...register("brNo", {
pattern: /[0-9]{8}/,
})}
error={Boolean(errors.brNo)}
helperText={Boolean(errors.brNo) && t("Please input correct customer br no.")}
/>
</Grid>
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions>
</CardContent>
</Card>
);
};

export default CustomerDetails;

+ 214
- 0
src/components/CreateCustomer/SubsidiaryAllocation.tsx Прегледај датотеку

@@ -0,0 +1,214 @@
"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,
FormControl,
InputLabel,
Select,
MenuItem,
Box,
Button,
Card,
CardActions,
CardContent,
TabsProps,
Tab,
Tabs,
SelectChangeEvent,
} from "@mui/material";
import differenceBy from "lodash/differenceBy";
import uniq from "lodash/uniq";
import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { CustomerFormInputs } from "@/app/api/customer/actions";
import { Subsidiary } from "@/app/api/customer";

interface Props {
subsidiaries: Subsidiary[];
}

const SubsidiaryAllocation: React.FC<Props> = ({
subsidiaries,
}) => {
const { t } = useTranslation();
const { setValue, getValues } = useFormContext<CustomerFormInputs>();

const [filteredSubsidiary, setFilteredSubsidiary] = React.useState(subsidiaries);
const [selectedSubsidiary, setSelectedSubsidiary] = React.useState<
typeof filteredSubsidiary
>(
subsidiaries.filter((subsidiary) =>
getValues("addSubsidiaryIds")?.includes(subsidiary.id),
),
);

// Adding / Removing staff
const addSubsidiary = React.useCallback((subsidiary: Subsidiary) => {
setSelectedSubsidiary((subsidiaries) => [...subsidiaries, subsidiary]);
}, []);
const removeSubsidiary = React.useCallback((subsidiary: Subsidiary) => {
setSelectedSubsidiary((subsidiaries) => subsidiaries.filter((s) => s.id !== subsidiary.id));
}, []);
const clearSubsidiary = React.useCallback(() => {
setSelectedSubsidiary([]);
}, []);
// Sync with form
useEffect(() => {
setValue(
"addSubsidiaryIds",
selectedSubsidiary.map((subsidiary) => subsidiary.id),
);
}, [selectedSubsidiary, setValue]);

const subsidiaryPoolColumns = React.useMemo<Column<Subsidiary>[]>(
() => [
{
label: t("Add"),
name: "id",
onClick: addSubsidiary,
buttonIcon: <PersonAdd />,
},
{ label: t("Subsidiary Code"), name: "code" },
{ label: t("Subsidiary Name"), name: "name" },
{ label: t("Subsidiary Br No."), name: "brNo" },
{ label: t("Subsidiary Contact Name"), name: "contactName" },
{ label: t("Subsidiary Phone"), name: "phone" },
{ label: t("Subsidiary Address"), name: "address" },
{ label: t("Subsidiary District"), name: "district" },
{ label: t("Subsidiary Email"), name: "email" },
],
[addSubsidiary, t],
);

const allocatedSubsidiaryColumns = React.useMemo<Column<Subsidiary>[]>(
() => [
{
label: t("Remove"),
name: "id",
onClick: removeSubsidiary,
buttonIcon: <PersonRemove />,
},
{ label: t("Subsidiary Code"), name: "code" },
{ label: t("Subsidiary Name"), name: "name" },
{ label: t("Subsidiary Br No."), name: "brNo" },
{ label: t("Subsidiary Contact Name"), name: "contactName" },
{ label: t("Subsidiary Phone"), name: "phone" },
{ label: t("Subsidiary Address"), name: "address" },
{ label: t("Subsidiary District"), name: "district" },
{ label: t("Subsidiary Email"), name: "email" },
],
[removeSubsidiary, 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(() => {
setFilteredSubsidiary(
subsidiaries.filter((subsidiary) => {
const q = query.toLowerCase();
return (
(subsidiary.name.toLowerCase().includes(q) ||
subsidiary.code.toString().includes(q) ||
(subsidiary.brNo != null && subsidiary.brNo.toLowerCase().includes(q)))
);
}),
);
}, [subsidiaries, query]);

// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

const reset = React.useCallback(() => {
clearQueryInput();
clearSubsidiary();
}, [clearQueryInput, clearSubsidiary]);

return (
<>
<Card sx={{ display: "block"}}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("Subsidiary 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 subsidiary 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("Subsidiary Pool")} />
<Tab
label={`${t("Allocated Subsidiary")} (${selectedSubsidiary.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredSubsidiary, selectedSubsidiary, "id")}
columns={subsidiaryPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedSubsidiary}
columns={allocatedSubsidiaryColumns}
/>
)}
</Box>
</Stack>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />} onClick={reset}>
{t("Reset")}
</Button>
</CardActions>
</CardContent>
</Card>
</>
);
};

export default SubsidiaryAllocation;

+ 1
- 0
src/components/CreateCustomer/index.ts Прегледај датотеку

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

+ 70
- 0
src/components/CustomerSearch/CustomerSearch.tsx Прегледај датотеку

@@ -0,0 +1,70 @@
"use client";

import { Customer } from "@/app/api/customer";
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";

interface Props {
customers: Customer[];
}

type SearchQuery = Partial<Omit<Customer, "id">>;
type SearchParamNames = keyof SearchQuery;

const CustomerSearch: React.FC<Props> = ({ customers }) => {
const { t } = useTranslation();

const [filteredCustomers, setFilteredCustomers] = useState(customers);
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Customer Code"), paramName: "code", type: "text" },
{ label: t("Customer Name"), paramName: "name", type: "text" },
],
[t],
);
const onReset = useCallback(() => {
setFilteredCustomers(customers);
}, [customers]);

const onTaskClick = useCallback((customer: Customer) => {
console.log(customer);
}, []);

const columns = useMemo<Column<Customer>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onTaskClick,
buttonIcon: <EditNote />,
},
{ name: "code", label: t("Customer Code") },
{ name: "name", label: t("Customer Name") },
],
[onTaskClick, t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredCustomers(
customers.filter(
(customer) =>
customer.code.toLowerCase().includes(query.code.toLowerCase()) &&
customer.name.toLowerCase().includes(query.name.toLowerCase()),
),
);
}}
onReset={onReset}
/>
<SearchResults items={filteredCustomers} columns={columns} />
</>
);
};

export default CustomerSearch;

+ 38
- 0
src/components/CustomerSearch/CustomerSearchLoading.tsx Прегледај датотеку

@@ -0,0 +1,38 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const CustomerSearchLoading: 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 CustomerSearchLoading;

+ 18
- 0
src/components/CustomerSearch/CustomerSearchWrapper.tsx Прегледај датотеку

@@ -0,0 +1,18 @@
import { fetchAllCustomers } from "@/app/api/customer";
import React from "react";
import CustomerSearch from "./CustomerSearch";
import CustomerSearchLoading from "./CustomerSearchLoading";

interface SubComponents {
Loading: typeof CustomerSearchLoading;
}

const CustomerSearchWrapper: React.FC & SubComponents = async () => {
const customers = await fetchAllCustomers();

return <CustomerSearch customers={customers} />;
};

CustomerSearchWrapper.Loading = CustomerSearchLoading;

export default CustomerSearchWrapper;

+ 1
- 0
src/components/CustomerSearch/index.ts Прегледај датотеку

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

+ 16
- 16
src/components/SearchBox/SearchBox.tsx Прегледај датотеку

@@ -177,22 +177,22 @@ function SearchBox<T extends string>({
);
})}
</Grid>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
);


+ 13
- 0
src/components/SearchResults/SearchResults.tsx Прегледај датотеку

@@ -12,6 +12,8 @@ import TablePagination, {
} from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow";
import IconButton from "@mui/material/IconButton";
import { ThemeProvider, createTheme } from "@mui/material";
import { zhTW, enUS } from '@mui/material/locale';

export interface ResultWithId {
id: string | number;
@@ -65,8 +67,18 @@ function SearchResults<T extends ResultWithId>({
setPage(0);
};

const theme = createTheme(

// locale
//TODO: May need to know what locale the client is using
// localStorage.getItem("locale")?.includes("zh") ? zhTW : enUS
zhTW
);

const table = (
<>
<ThemeProvider theme={theme}>
<TableContainer sx={{ maxHeight: 440 }}>
<Table stickyHeader>
<TableHead>
@@ -117,6 +129,7 @@ function SearchResults<T extends ResultWithId>({
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</ThemeProvider>
</>
);



+ 6
- 0
src/i18n/en/breadcrumb.json Прегледај датотеку

@@ -0,0 +1,6 @@
{
"Overview": "Overview",

"customer": "Customer",
"Create Customer": "Create Customer"
}

+ 7
- 1
src/i18n/en/common.json Прегледај датотеку

@@ -1,3 +1,9 @@
{
"Grade {{grade}}": "Grade {{grade}}"
"Grade {{grade}}": "Grade {{grade}}",
"Search": "Search",
"Search Criteria": "Search Criteria",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Reset": "Reset"
}

+ 42
- 0
src/i18n/en/customer.json Прегледај датотеку

@@ -0,0 +1,42 @@
{
"Customer": "Client",
"Create Customer": "Create Client",
"Edit Customer": "Edit Client",
"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",

"Please input correct customer code": "Please input correct client code",
"Please input correct customer name": "Please input correct client name",
"Please input correct customer email": "Please input correct client email",
"Please input correct customer br no.": "Please input correct client br no.",

"Subsidiary" : "Subsidiary",
"Subsidiary Allocation": "Subsidiary Allocation",
"Search by subsidiary code, name or br no.": "Search by subsidiary code, name or br no.",
"Subsidiary Pool": "Subsidiary Pool",
"Allocated Subsidiary": "Allocated 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",

"Add": "Add",
"Details": "Details",
"Search": "Search",
"Search Criteria": "Search Criteria",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Reset": "Reset"
}

+ 6
- 0
src/i18n/zh/breadcrumb.json Прегледај датотеку

@@ -0,0 +1,6 @@
{
"Overview": "總覽",

"customer": "客戶",
"Create Customer": "建立客戶"
}

+ 7
- 1
src/i18n/zh/common.json Прегледај датотеку

@@ -1 +1,7 @@
{}
{
"Search": "搜尋",
"Search Criteria": "搜尋條件",
"Cancel": "取消",
"Confirm": "確認",
"Reset": "重置"
}

+ 42
- 0
src/i18n/zh/customer.json Прегледај датотеку

@@ -0,0 +1,42 @@
{
"Customer": "客戶",
"Create Customer": "建立客戶",
"Edit Customer": "編輯客戶",
"Customer Code": "客戶編號",
"Customer Name": "客戶名稱",
"Customer Address": "客戶地址",
"Customer District": "客戶地區",
"Customer Email": "客戶電郵",
"Customer Phone": "客戶電話",
"Customer Contact Name": "客戶聯絡名稱",
"Customer Br No.": "客戶商業登記號碼",
"Customer Details": "客戶詳請",

"Please input correct customer code": "請輸入客戶編號",
"Please input correct customer name": "請輸入客戶編號",
"Please input correct customer email": "請輸入正確客戶電郵",
"Please input correct customer br no.": "請輸入正確客戶商業登記號碼",

"Subsidiary": "子公司",
"Subsidiary Allocation": "子公司分配",
"Search by subsidiary code, name or br no.": "可使用關鍵字搜尋 (子公司編號, 名稱或商業登記號碼)",
"Subsidiary Pool": "所有子公司",
"Allocated Subsidiary": "已分配的子公司",
"Subsidiary Code": "子公司編號",
"Subsidiary Name": "子公司名稱",
"Subsidiary Address": "子公司地址",
"Subsidiary District": "子公司地區",
"Subsidiary Email": "子公司電郵",
"Subsidiary Phone": "子公司電話",
"Subsidiary Contact Name": "子公司聯絡名稱",
"Subsidiary Br No.": "子公司商業登記號碼",
"Subsidiary Details": "子公司詳請",

"Add": "新增",
"Details": "詳請",
"Search": "搜尋",
"Search Criteria": "搜尋條件",
"Cancel": "取消",
"Confirm": "確認",
"Reset": "重置"
}

Loading…
Откажи
Сачувај