Переглянути джерело

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_30082024_FRONTEND_UAT
leoho2fi 1 рік тому
джерело
коміт
533e0456ac
34 змінених файлів з 761 додано та 67 видалено
  1. +25
    -0
      src/app/(main)/invoice/new/page.tsx
  2. +35
    -1
      src/app/(main)/invoice/page.tsx
  3. +9
    -7
      src/app/(main)/settings/company/create/page.tsx
  4. +1
    -1
      src/app/(main)/settings/customer/create/page.tsx
  5. +8
    -7
      src/app/(main)/settings/salary/page.tsx
  6. +26
    -0
      src/app/api/companys/actions.ts
  7. +35
    -0
      src/app/api/invoices/actions.ts
  8. +26
    -0
      src/app/api/invoices/index.ts
  9. +1
    -1
      src/app/api/salarys/actions.ts
  10. +3
    -2
      src/app/api/salarys/index.ts
  11. +1
    -1
      src/app/api/staff/actions.ts
  12. +5
    -0
      src/app/utils/formatUtil.ts
  13. +225
    -0
      src/components/CreateCompany/CompanyDetails.tsx
  14. +106
    -0
      src/components/CreateCompany/CreateCompany.tsx
  15. +10
    -0
      src/components/CreateCompany/CreateCompanyWrapper.tsx
  16. +1
    -0
      src/components/CreateCompany/index.ts
  17. +15
    -15
      src/components/CreateStaff/CreateStaff.tsx
  18. +1
    -1
      src/components/CustomInputForm/CustomInputForm.tsx
  19. +1
    -1
      src/components/CustomerDetail/ContactInfo.tsx
  20. +2
    -2
      src/components/CustomerDetail/CustomerDetail.tsx
  21. +1
    -1
      src/components/CustomerDetail/CustomerInfo.tsx
  22. +31
    -18
      src/components/EditStaff/EditStaff.tsx
  23. +91
    -0
      src/components/InvoiceSearch/InvoiceSearch.tsx
  24. +40
    -0
      src/components/InvoiceSearch/InvoiceSearchLoading.tsx
  25. +21
    -0
      src/components/InvoiceSearch/InvoiceSearchWrapper.tsx
  26. +1
    -0
      src/components/InvoiceSearch/index.ts
  27. +4
    -2
      src/components/SalarySearch/SalarySearch.tsx
  28. +16
    -1
      src/components/SalarySearch/SalarySearchWrapper.tsx
  29. +1
    -1
      src/components/SubsidiaryDetail/ContactInfo.tsx
  30. +1
    -1
      src/components/SubsidiaryDetail/SubsidiaryDetail.tsx
  31. +2
    -2
      src/i18n/en/breadcrumb.json
  32. +7
    -0
      src/i18n/en/salarys.json
  33. +2
    -2
      src/i18n/en/subsidiary.json
  34. +7
    -0
      src/i18n/zh/salarys.json

+ 25
- 0
src/app/(main)/invoice/new/page.tsx Переглянути файл

@@ -0,0 +1,25 @@
import { Metadata } from "next";
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 Link from "next/link";

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

const Invoice: React.FC = async () => {
const { t } = await getServerI18n("Create Invoice");

return (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Create Invoice")}
</Typography>
</>
)
};

export default Invoice;

+ 35
- 1
src/app/(main)/invoice/page.tsx Переглянути файл

@@ -1,11 +1,45 @@
import { Metadata } from "next";
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 Link from "next/link";
import { Suspense } from "react";
import InvoiceSearch from "@/components/InvoiceSearch";

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

const Invoice: React.FC = async () => {
return "Invoice";
const { t } = await getServerI18n("Invoice");

return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Invoice")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/invoice/new"
>
{t("Create Invoice")}
</Button>
</Stack>
<Suspense fallback={<InvoiceSearch.Loading />}>
<InvoiceSearch />
</Suspense>
</>
)
};

export default Invoice;

+ 9
- 7
src/app/(main)/settings/company/create/page.tsx Переглянути файл

@@ -1,20 +1,22 @@
import { fetchProjectCategories } from "@/app/api/projects";
import { preloadStaff } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "@/components/CreateProject";
import CreateCompany from "@/components/CreateCompany";
import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";

export const metadata: Metadata = {
title: "Create Project",
title: "Create Comapny",
};
const Companys: React.FC = async () => {
const { t } = await getServerI18n("projects");
const { t } = await getServerI18n("companys");
return(
<>AAAA</>
<>
<Typography variant="h4">{t("Create Company")}</Typography>
<I18nProvider namespaces={["companys"]}>
<CreateCompany />
</I18nProvider>
</>
)

}


+ 1
- 1
src/app/(main)/settings/customer/create/page.tsx Переглянути файл

@@ -6,7 +6,7 @@ import Typography from "@mui/material/Typography";
import { Metadata } from "next";

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

const CreateCustomer: React.FC = async () => {


+ 8
- 7
src/app/(main)/settings/salary/page.tsx Переглянути файл

@@ -1,6 +1,6 @@
import SalarySearch from "@/components/SalarySearch";
import { Metadata } from "next";
import { getServerI18n } from "@/i18n";
import { I18nProvider, getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
@@ -15,7 +15,6 @@ export const metadata: Metadata = {

const Salary: React.FC = async () => {
const { t } = await getServerI18n("Salary");
// Preload necessary dependencies
// fetchSalarys();
// preloadSalarys();
@@ -31,18 +30,20 @@ const Salary: React.FC = async () => {
<Typography variant="h4" marginInlineEnd={2}>
{t("Salary")}
</Typography>
<Button
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/position/new"
>
{t("Create Salary")}
</Button>
</Button> */}
</Stack>
<Suspense fallback={<SalarySearch.Loading />}>
<SalarySearch/>
</Suspense>
<I18nProvider namespaces={["salarys"]}>
<Suspense fallback={<SalarySearch.Loading />}>
<SalarySearch/>
</Suspense>
</I18nProvider>
</>
)
};


+ 26
- 0
src/app/api/companys/actions.ts Переглянути файл

@@ -1,6 +1,8 @@
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Dayjs } from "dayjs";
import { cache } from "react";

export interface comboProp {
@@ -12,6 +14,30 @@ export interface combo {
records: comboProp[];
}

export interface CreateCompanyInputs {
companyCode: string;
companyName: string;
brNo: string;
contactName: string;
phone: string;
otHourTo: string;
otHourFrom: string;
normalHourTo: string;
normalHourFrom: string;
currency: string;
address: string;
district: string;
email: string;
}

export const saveCompany = async (data: CreateCompanyInputs) => {
return serverFetchJson(`${BASE_API_URL}/companys/new`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const fetchCompanyCombo = cache(async () => {
return serverFetchJson<combo>(`${BASE_API_URL}/companys/combo`, {
next: { tags: ["company"] },


+ 35
- 0
src/app/api/invoices/actions.ts Переглянути файл

@@ -0,0 +1,35 @@
"use server"

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";


export interface comboProp {
id: any;
label: string;
}

export interface combo {
records: comboProp[];
}
export interface CreateDepartmentInputs {
departmentCode: string;
departmentName: string;
description: string;
}

export const saveDepartment = async (data: CreateDepartmentInputs) => {
return serverFetchJson(`${BASE_API_URL}/departments/new`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};


export const fetchDepartmentCombo = cache(async () => {
return serverFetchJson<combo>(`${BASE_API_URL}/departments/combo`, {
next: { tags: ["department"] },
});
});

+ 26
- 0
src/app/api/invoices/index.ts Переглянути файл

@@ -0,0 +1,26 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";

export interface InvoiceResult {
id: number;
projectCode: string;
projectName: string;
stage: String;
comingPaymentMileStone: String;
paymentMilestoneDate: String;
resourceUsage: number;
unbilledHours: number;
reminder: String;
}

export const preloadInvoices = () => {
fetchInvoices();
};

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

+ 1
- 1
src/app/api/salarys/actions.ts Переглянути файл

@@ -14,7 +14,7 @@ export interface combo {
}
export const fetchSalaryCombo = cache(async () => {
return serverFetchJson<combo>(`${BASE_API_URL}/salary/combo`, {
return serverFetchJson<combo>(`${BASE_API_URL}/salarys/combo`, {
next: { tags: ["salary"] },
});
});

+ 3
- 2
src/app/api/salarys/index.ts Переглянути файл

@@ -5,10 +5,11 @@ import "server-only";

export interface SalaryResult {
id: number;
lowerLimit: number;
upperLimit: number;
lowerLimit: String;
upperLimit: String;
salaryPoint: number;
salary: number;
hourlyRate: String;
}

export const preloadSalarys = () => {


+ 1
- 1
src/app/api/staff/actions.ts Переглянути файл

@@ -19,7 +19,7 @@ export interface CreateStaffInputs {
companyId: number;
gradeId: number;
teamId: number;
salaryEffId: number;
salaryId: number;
email: string;
phone1: string;
phone2: string;


+ 5
- 0
src/app/utils/formatUtil.ts Переглянути файл

@@ -38,3 +38,8 @@ export const shortDateFormatter = (locale?: string) => {
return shortDateFormatter_en;
}
};

export function convertLocaleStringToNumber(numberString: String): number {
const numberWithoutCommas = numberString.replace(/,/g, "");
return parseFloat(numberWithoutCommas);
}

+ 225
- 0
src/components/CreateCompany/CompanyDetails.tsx Переглянути файл

@@ -0,0 +1,225 @@
"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 FormControl from "@mui/material/FormControl";
import Grid from "@mui/material/Grid";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
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, UseFormRegister, useFormContext } from "react-hook-form";
import { CreateCompanyInputs } from "@/app/api/companys/actions";
import { TimePicker } from "@mui/x-date-pickers";
import dayjs from 'dayjs';

const CompanyDetails: React.FC = ({
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
control,
setValue,
getValues,
} = useFormContext<CreateCompanyInputs>();


return (
<Card>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Company Details")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Company Code")}
fullWidth
{...register("companyCode", {
required: "Company code required!",
})}
error={Boolean(errors.companyCode)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company Name")}
fullWidth
{...register("companyName", {
required: "Company name required!",
})}
error={Boolean(errors.companyName)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company brNo")}
fullWidth
{...register("brNo", {
required: "Please enter a brNo",
})}
error={Boolean(errors.brNo)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company contact name")}
fullWidth
{...register("contactName", {
required: "Please enter a contact name",
})}
error={Boolean(errors.contactName)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company contact number")}
fullWidth
{...register("phone", {
required: "Please enter a contact number",
})}
error={Boolean(errors.phone)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company contact email")}
fullWidth
{...register("email", {
required: "Please enter a contact email",
})}
error={Boolean(errors.email)}
/>
</Grid>
<Grid item xs={3}>
<Controller
control={control}
name="normalHourFrom"
rules={{ required: true }}
render={({ field }) => {
return (
<TimePicker
label="Normal Hour From"
inputRef={field.ref}
onChange={(time) => {
const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : '';
field.onChange(formattedTime);
}}
sx={{ width: '100%' }}
/>
);
}}
/>
</Grid>
<Grid item xs={3}>
<Controller
control={control}
name="normalHourTo"
rules={{ required: true }}
render={({ field }) => {
return (
<TimePicker
label="Normal Hour To"
inputRef={field.ref}
onChange={(time) => {
const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : '';
field.onChange(formattedTime);
}}
sx={{ width: '100%' }}
/>
);
}}
/>
</Grid>
<Grid item xs={3}>
<Controller
control={control}
name="otHourFrom"
rules={{ required: true }}
render={({ field }) => {
return (
<TimePicker
label="OT Hour From"
inputRef={field.ref}
onChange={(time) => {
const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : '';
field.onChange(formattedTime);
}}
sx={{ width: '100%' }}
/>
);
}}
/>
</Grid>
<Grid item xs={3}>
<Controller
control={control}
name="otHourTo"
rules={{ required: true }}
render={({ field }) => {
return (
<TimePicker
label="OT Hour To"
inputRef={field.ref}
onChange={(time) => {
const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : '';
field.onChange(formattedTime);
}}
sx={{ width: '100%' }}
/>
);
}}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company address")}
fullWidth
{...register("address", {
required: "Please enter a address",
})}
error={Boolean(errors.address)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Company district")}
fullWidth
{...register("district", {
required: "Please enter a district",
})}
error={Boolean(errors.district)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("currency")}
fullWidth
{...register("currency", {
required: "Please enter a currency",
})}
error={Boolean(errors.currency)}
/>
</Grid>
</Grid>
</Box>
{/* <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")}
</Button>
</CardActions> */}
</CardContent>
</Card>
);
};

export default CompanyDetails;

+ 106
- 0
src/components/CreateCompany/CreateCompany.tsx Переглянути файл

@@ -0,0 +1,106 @@
"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 { CreateCompanyInputs, saveCompany } from "@/app/api/companys/actions";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import CompanyDetails from "./CompanyDetails";
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import dayjs from "dayjs";

const CreateCompany: React.FC = ({

}) => {
const [serverError, setServerError] = useState("");
const { t } = useTranslation();
const router = useRouter();

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

const onSubmit = useCallback<SubmitHandler<CreateCompanyInputs>>(
async (data) => {
try {
console.log(data);
setServerError("");
// console.log(JSON.stringify(data));
await saveCompany(data)
router.replace("/settings/company");
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t],
);

const onSubmitError = useCallback<SubmitErrorHandler<CreateCompanyInputs>>(
(errors) => {
console.log(errors)
},
[],
);

const formProps = useForm<CreateCompanyInputs>({
defaultValues: {
companyCode: "",
companyName: "",
brNo: "",
contactName: "",
phone: "",
otHourTo: "",
otHourFrom: "",
normalHourTo: dayjs().format('HH:mm:ss'),
normalHourFrom: dayjs().format('HH:mm:ss'),
currency: "",
address: "",
district: "",
email: "",
},
});
const errors = formProps.formState.errors;

return(
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
{
<LocalizationProvider dateAdapter={AdapterDayjs}>
<CompanyDetails />
</LocalizationProvider>
}

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

+ 10
- 0
src/components/CreateCompany/CreateCompanyWrapper.tsx Переглянути файл

@@ -0,0 +1,10 @@
import CreateCompany from "./CreateCompany";

const CreateCompanyWrapper: React.FC = async () => {
return (
<CreateCompany
/>
)
}

export default CreateCompanyWrapper;

+ 1
- 0
src/components/CreateCompany/index.ts Переглянути файл

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

+ 15
- 15
src/components/CreateStaff/CreateStaff.tsx Переглянути файл

@@ -179,7 +179,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
label: t("Team"),
type: "combo-Obj",
options: teamCombo,
required: true,
required: false,
},
{
id: "departmentId",
@@ -193,14 +193,14 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
label: t("Grade"),
type: "combo-Obj",
options: gradeCombo,
required: true,
required: false,
},
{
id: "skillSetId",
label: t("Skillset"),
type: "combo-Obj",
options: skillCombo,
required: true,
required: false,
},
{
id: "currentPositionId",
@@ -210,19 +210,19 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
required: true,
},
{
id: "salaryEffId",
id: "salaryId",
label: t("Salary Point"),
type: "combo-Obj",
options: salaryCombo,
required: true,
},
{
id: "hourlyRate",
label: t("Hourly Rate"),
type: "numeric-testing",
value: "",
required: true,
},
// {
// id: "hourlyRate",
// label: t("Hourly Rate"),
// type: "numeric-testing",
// value: "",
// required: false,
// },
{
id: "employType",
label: t("Employ Type"),
@@ -245,7 +245,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
label: t("Phone1"),
type: "text",
value: "",
pattern: "^\\d{8}$",
// pattern: "^\\d{8}$",
message: t("input correct phone no."),
required: true,
},
@@ -254,9 +254,9 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
label: t("Phone2"),
type: "text",
value: "",
pattern: "^\\d{8}$",
// pattern: "^\\d{8}$",
message: t("input correct phone no."),
required: true,
required: false,
},
],
[
@@ -272,7 +272,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
label: t("Emergency Contact Phone"),
type: "text",
value: "",
pattern: "^\\d{8}$",
// pattern: "^\\d{8}$",
message: t("input correct phone no."),
required: true,
},


+ 1
- 1
src/components/CustomInputForm/CustomInputForm.tsx Переглянути файл

@@ -274,7 +274,7 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({
fullWidth
{...register(field.id, {
pattern:
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/,
/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/,
})}
defaultValue={!field.value ? `${field.value}` : ""}
required={field.required ?? false}


+ 1
- 1
src/components/CustomerDetail/ContactInfo.tsx Переглянути файл

@@ -231,7 +231,7 @@ const ContactInfo: React.FC<Props> = ({
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)))
const errorRows_EmailFormat = rows.filter(row => !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email)))
if (errorRows_EmailFormat.length > 0) {
setError("addContacts", { message: "Contact details include empty fields", type: "email_format" })
} else {


+ 2
- 2
src/components/CustomerDetail/CustomerDetail.tsx Переглянути файл

@@ -167,7 +167,7 @@ const CustomerDetail: React.FC<Props> = ({
formProps.setError("code", { message: "Code is empty", type: "required" })
}

// if (data.email && data.email?.length > 0 && !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(data.email)) {
// if (data.email && data.email?.length > 0 && !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(data.email)) {
// haveError = true
// formProps.setError("email", { message: "Email format is not valid", type: "custom" })
// }
@@ -182,7 +182,7 @@ const CustomerDetail: React.FC<Props> = ({
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-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email))).length > 0) {
haveError = true
formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" })
}


+ 1
- 1
src/components/CustomerDetail/CustomerInfo.tsx Переглянути файл

@@ -107,7 +107,7 @@ const CustomerInfo: React.FC<Props> = ({
label={t("Customer Email")}
fullWidth
{...register("email", {
pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/,
pattern: /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/,
})}
error={Boolean(errors.email)}
helperText={Boolean(errors.email) && t("Please input correct customer email")}


+ 31
- 18
src/components/EditStaff/EditStaff.tsx Переглянути файл

@@ -77,7 +77,7 @@ const EditStaff: React.FC = async () => {
"grade",
"skill",
"currentPosition",
"salaryEffective",
"salary",
"hourlyRate",
"employType",
"email",
@@ -140,6 +140,7 @@ const EditStaff: React.FC = async () => {
label: t(`Staff ID`),
type: "text",
value: data[key] ?? "",
required: true,
};
case "name":
return {
@@ -147,6 +148,7 @@ const EditStaff: React.FC = async () => {
label: t(`Staff Name`),
type: "text",
value: data[key] ?? "",
required: true,
};
case "company":
return {
@@ -155,6 +157,7 @@ const EditStaff: React.FC = async () => {
type: "combo-Obj",
options: companyCombo,
value: data[key].id ?? "",
required: true,
};
case "team":
return {
@@ -171,6 +174,7 @@ const EditStaff: React.FC = async () => {
type: "combo-Obj",
options: departmentCombo,
value: data[key]?.id ?? "",
required: true,
// later check
};
case "grade":
@@ -179,7 +183,7 @@ const EditStaff: React.FC = async () => {
label: t(`Grade`),
type: "combo-Obj",
options: gradeCombo,
value: data[key].id ?? "",
value: data[key] !== null ? data[key].id ?? "" : "",
};
case "skill":
return {
@@ -187,7 +191,7 @@ const EditStaff: React.FC = async () => {
label: t(`Skillset`),
type: "combo-Obj",
options: skillCombo,
value: data[key].id ?? "",
value: data[key] !== null ? data[key].id ?? "" : "",
};
case "currentPosition":
return {
@@ -196,24 +200,26 @@ const EditStaff: React.FC = async () => {
type: "combo-Obj",
options: positionCombo,
value: data[key].id ?? "",
required: true,
};
case "salaryEffective":
case "salary":
return {
id: `salaryEffId`,
id: `salaryId`,
label: t(`Salary Point`),
type: "combo-Obj",
options: salaryCombo,
value: data[key].salary.id ?? "",
};
case "hourlyRate":
return {
id: `${key}`,
label: t(`hourlyRate`),
type: "text",
value: "",
// value: data[key],
readOnly: true,
value: data[key] !== null ? data[key].id ?? "" : "",
required: true,
};
// case "hourlyRate":
// return {
// id: `${key}`,
// label: t(`hourlyRate`),
// type: "text",
// value: "",
// // value: data[key],
// readOnly: true,
// };
case "employType":
return {
id: `${key}`,
@@ -221,6 +227,7 @@ const EditStaff: React.FC = async () => {
type: "combo-Obj",
options: employTypeCombo,
value: data[key] ?? "",
required: true,
};
case "email":
return {
@@ -230,22 +237,24 @@ const EditStaff: React.FC = async () => {
value: data[key] ?? "",
pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$",
message: t("input matching format"),
required: true,
};
case "phone1":
return {
id: `${key}`,
label: t(`Phone1`),
type: "text",
pattern: "^\\d{8}$",
// pattern: "^\\d{8}$",
message: t("input correct phone no."),
value: data[key] ?? "",
required: true,
};
case "phone2":
return {
id: `${key}`,
label: t(`Phone2`),
type: "text",
pattern: "^\\d{8}$",
// pattern: "^\\d{8}$",
message: t("input correct phone no."),
value: data[key] ?? "",
} as Field;
@@ -263,15 +272,17 @@ const EditStaff: React.FC = async () => {
label: t(`Emergency Contact Name`),
type: "text",
value: data[key] ?? "",
required: true,
} as Field;
case "emergContactPhone":
return {
id: `${key}`,
label: t(`Emergency Contact Phonee`),
type: "text",
pattern: "^\\d{8}$",
// pattern: "^\\d{8}$",
message: t("input correct phone no."),
value: data[key] ?? "",
required: true,
} as Field;
case "joinDate":
return {
@@ -279,6 +290,7 @@ const EditStaff: React.FC = async () => {
label: t(`Join Date`),
type: "multiDate",
value: data[key] ?? "",
required: true,
} as Field;
case "joinPosition":
return {
@@ -287,6 +299,7 @@ const EditStaff: React.FC = async () => {
type: "combo-Obj",
options: positionCombo,
value: data[key].id ?? "",
required: true,
} as Field;
case "departDate":
return {


+ 91
- 0
src/components/InvoiceSearch/InvoiceSearch.tsx Переглянути файл

@@ -0,0 +1,91 @@
"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 { InvoiceResult } from "@/app/api/invoices";

interface Props {
invoices: InvoiceResult[];
}

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

const InvoiceSearch: React.FC<Props> = ({ invoices }) => {
const { t } = useTranslation("invoices");

const [filteredInvoices, setFilteredInvoices] = useState(invoices);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Project code"), paramName: "projectCode", type: "text" },
{ label: t("Project name"), paramName: "projectName", type: "text" },
// { label: t("Stage"), paramName: "stage", type: "text" },
{ label: t("Coming payment milestone"), paramName: "comingPaymentMileStone", type: "text" },
{ label: t("Payment date"), paramName: "paymentMilestoneDate", type: "text" },
// { label: t("Resource utilization %"), paramName: "resourceUsage", type: "text" },
// { label: t("Unbilled hours"), paramName: "unbilledHours", type: "text" },
// { label: t("Reminder to issue invoice"), paramName: "reminder", type: "text" },
],
[t, invoices],
);

const onReset = useCallback(() => {
setFilteredInvoices(invoices);
}, [invoices]);

const onProjectClick = useCallback((project: InvoiceResult) => {
console.log(project);
}, []);

const columns = useMemo<Column<InvoiceResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onProjectClick,
buttonIcon: <EditNote />,
},
{ name: "projectCode", label: t("Project code") },
{ name: "projectName", label: t("Project name") },
{ name: "stage", label: t("Stage") },
{ name: "comingPaymentMileStone", label: t("Coming payment milestone") },
{ name: "paymentMilestoneDate", label: t("Payment date") },
{ name: "resourceUsage", label: t("Resource utilization %") },
{ name: "unbilledHours", label: t("Unbilled hours") },
{ name: "reminder", label: t("Reminder to issue invoice") },
],
[t, onProjectClick],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredInvoices(
invoices.filter(
(d) =>
d.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) &&
d.projectName.toLowerCase().includes(query.projectName.toLowerCase()) &&
d.stage.toLowerCase().includes(query.stage.toLowerCase()) &&
{/*(query.client === "All" || p.client === query.client) &&
(query.category === "All" || p.category === query.category) &&
(query.team === "All" || p.team === query.team), **/}
),
);
}}
onReset={onReset}
/>
<SearchResults<InvoiceResult>
items={filteredInvoices}
columns={columns}
/>
</>
);
};

export default InvoiceSearch;

+ 40
- 0
src/components/InvoiceSearch/InvoiceSearchLoading.tsx Переглянути файл

@@ -0,0 +1,40 @@
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 InvoiceSearchLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>Invoice
<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 InvoiceSearchLoading;

+ 21
- 0
src/components/InvoiceSearch/InvoiceSearchWrapper.tsx Переглянути файл

@@ -0,0 +1,21 @@

import React from "react";
import InvoiceSearch from "./InvoiceSearch";
import InvoiceSearchLoading from "./InvoiceSearchLoading";
// For Later use
import { fetchInvoices } from "@/app/api/invoices";

interface SubComponents {
Loading: typeof InvoiceSearchLoading;
}

const InvoiceSearchWrapper: React.FC & SubComponents = async () => {
// For Later use
const Invoices = await fetchInvoices();

return <InvoiceSearch invoices={Invoices} />;
};

InvoiceSearchWrapper.Loading = InvoiceSearchLoading;

export default InvoiceSearchWrapper;

+ 1
- 0
src/components/InvoiceSearch/index.ts Переглянути файл

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

+ 4
- 2
src/components/SalarySearch/SalarySearch.tsx Переглянути файл

@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import { SalaryResult } from "@/app/api/salarys";
import { convertLocaleStringToNumber } from "@/app/utils/formatUtil"

interface Props {
salarys: SalaryResult[];
@@ -46,6 +47,7 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => {
{ name: "salaryPoint", label: t("Salary Point") },
{ name: "lowerLimit", label: t("Lower Limit") },
{ name: "upperLimit", label: t("Upper Limit") },
{ name: "hourlyRate", label: t("Hourly Rate") },
],
[t, onSalaryClick],
);
@@ -58,8 +60,8 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => {
setFilteredSalarys(
salarys.filter(
(s) =>
((s.lowerLimit <= Number(query.salary))&&
(s.upperLimit >= Number(query.salary)))||
((convertLocaleStringToNumber(s.lowerLimit) <= Number(query.salary))&&
(convertLocaleStringToNumber(s.upperLimit) >= Number(query.salary)))||
(s.salaryPoint === Number(query.salaryPoint))
),
);


+ 16
- 1
src/components/SalarySearch/SalarySearchWrapper.tsx Переглянути файл

@@ -8,11 +8,26 @@ interface SubComponents {
Loading: typeof SalarySearchLoading;
}

function calculateHourlyRate(loweLimit: number, upperLimit: number, numOfWorkingDay: number, workingHour: number){
const hourlyRate = (loweLimit + upperLimit)/2/numOfWorkingDay/workingHour
return hourlyRate.toLocaleString()
}

const SalarySearchWrapper: React.FC & SubComponents = async () => {
const Salarys = await fetchSalarys();
// const Salarys:any[] = []
const salarysWithHourlyRate = Salarys.map((salary) => {
const hourlyRate = calculateHourlyRate(Number(salary.lowerLimit), Number(salary.upperLimit),22, 8)
return {
...salary,
upperLimit: salary.upperLimit.toLocaleString(),
lowerLimit: salary.lowerLimit.toLocaleString(),
hourlyRate: hourlyRate
}
})
// console.log(salarysWithHourlyRate)

return <SalarySearch salarys={Salarys} />;
return <SalarySearch salarys={salarysWithHourlyRate} />;
};

SalarySearchWrapper.Loading = SalarySearchLoading;


+ 1
- 1
src/components/SubsidiaryDetail/ContactInfo.tsx Переглянути файл

@@ -231,7 +231,7 @@ const ContactInfo: React.FC<Props> = ({
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)))
const errorRows_EmailFormat = rows.filter(row => !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email)))

if (errorRows_EmailFormat.length > 0) {
setError("addContacts", { message: "Contact details include empty fields", type: "email_format" })


+ 1
- 1
src/components/SubsidiaryDetail/SubsidiaryDetail.tsx Переглянути файл

@@ -157,7 +157,7 @@ const SubsidiaryDetail: React.FC<Props> = ({
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-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email))).length > 0) {
haveError = true
formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" })
}


+ 2
- 2
src/i18n/en/breadcrumb.json Переглянути файл

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

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

+ 7
- 0
src/i18n/en/salarys.json Переглянути файл

@@ -0,0 +1,7 @@
{
"Details": "Details",
"Salary Point": "Salary Point",
"Lower Limit": "Lower Limit(HKD)",
"Upper Limit": "Upper Limit(HKD)",
"Hourly Rate": "Hourly Rate(HKD)"
}

+ 2
- 2
src/i18n/en/subsidiary.json Переглянути файл

@@ -15,8 +15,8 @@
"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",
"Customer Pool": "Client Pool",
"Allocated Customer": "Allocated Client",

"Please input correct subsidiary code": "Please input correct client code",
"Please input correct subsidiary name": "Please input correct client name",


+ 7
- 0
src/i18n/zh/salarys.json Переглянути файл

@@ -0,0 +1,7 @@
{
"Details": "詳請",
"Salary Point": "薪點",
"Lower Limit": "薪金下限(港元)",
"Upper Limit": "薪金上限(港元)",
"Hourly Rate": "時薪(港元)"
}

Завантаження…
Відмінити
Зберегти