diff --git a/src/app/(main)/invoice/new/page.tsx b/src/app/(main)/invoice/new/page.tsx new file mode 100644 index 0000000..6ac8255 --- /dev/null +++ b/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 ( + <> + + {t("Create Invoice")} + + + ) +}; + +export default Invoice; \ No newline at end of file diff --git a/src/app/(main)/invoice/page.tsx b/src/app/(main)/invoice/page.tsx index ae9fc37..d9d18bf 100644 --- a/src/app/(main)/invoice/page.tsx +++ b/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 ( + <> + + + {t("Invoice")} + + + + }> + + + + ) }; export default Invoice; diff --git a/src/app/(main)/settings/company/create/page.tsx b/src/app/(main)/settings/company/create/page.tsx index 1702f2d..e26aaf8 100644 --- a/src/app/(main)/settings/company/create/page.tsx +++ b/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 + <> + {t("Create Company")} + + + + ) } diff --git a/src/app/(main)/settings/customer/create/page.tsx b/src/app/(main)/settings/customer/create/page.tsx index c4f13b4..e0dc0e0 100644 --- a/src/app/(main)/settings/customer/create/page.tsx +++ b/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 () => { diff --git a/src/app/(main)/settings/salary/page.tsx b/src/app/(main)/settings/salary/page.tsx index d1c1d63..2531757 100644 --- a/src/app/(main)/settings/salary/page.tsx +++ b/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 () => { {t("Salary")} - + */} - }> - - + + }> + + + ) }; diff --git a/src/app/api/companys/actions.ts b/src/app/api/companys/actions.ts index d78f9b6..f342177 100644 --- a/src/app/api/companys/actions.ts +++ b/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(`${BASE_API_URL}/companys/combo`, { next: { tags: ["company"] }, diff --git a/src/app/api/invoices/actions.ts b/src/app/api/invoices/actions.ts new file mode 100644 index 0000000..c6bdfd2 --- /dev/null +++ b/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(`${BASE_API_URL}/departments/combo`, { + next: { tags: ["department"] }, + }); +}); \ No newline at end of file diff --git a/src/app/api/invoices/index.ts b/src/app/api/invoices/index.ts new file mode 100644 index 0000000..7c6fb55 --- /dev/null +++ b/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(`${BASE_API_URL}/invoices`, { + next: { tags: ["invoices"] }, + }); +}); \ No newline at end of file diff --git a/src/app/api/salarys/actions.ts b/src/app/api/salarys/actions.ts index 11384e0..7cd01d3 100644 --- a/src/app/api/salarys/actions.ts +++ b/src/app/api/salarys/actions.ts @@ -14,7 +14,7 @@ export interface combo { } export const fetchSalaryCombo = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/salary/combo`, { + return serverFetchJson(`${BASE_API_URL}/salarys/combo`, { next: { tags: ["salary"] }, }); }); \ No newline at end of file diff --git a/src/app/api/salarys/index.ts b/src/app/api/salarys/index.ts index 02fcf55..baed583 100644 --- a/src/app/api/salarys/index.ts +++ b/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 = () => { diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 8eb5ff3..1098508 100644 --- a/src/app/api/staff/actions.ts +++ b/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; diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 616d205..14d2ed1 100644 --- a/src/app/utils/formatUtil.ts +++ b/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); +} diff --git a/src/components/CreateCompany/CompanyDetails.tsx b/src/components/CreateCompany/CompanyDetails.tsx new file mode 100644 index 0000000..ccef906 --- /dev/null +++ b/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(); + + + return ( + + + + + {t("Company Details")} + + + + + + + + + + + + + + + + + + + + + + { + return ( + { + const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; + field.onChange(formattedTime); + }} + sx={{ width: '100%' }} + /> + ); + }} + /> + + + { + return ( + { + const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; + field.onChange(formattedTime); + }} + sx={{ width: '100%' }} + /> + ); + }} + /> + + + { + return ( + { + const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; + field.onChange(formattedTime); + }} + sx={{ width: '100%' }} + /> + ); + }} + /> + + + { + return ( + { + const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; + field.onChange(formattedTime); + }} + sx={{ width: '100%' }} + /> + ); + }} + /> + + + + + + + + + + + + + {/* + + */} + + + ); +}; + +export default CompanyDetails; \ No newline at end of file diff --git a/src/components/CreateCompany/CreateCompany.tsx b/src/components/CreateCompany/CreateCompany.tsx new file mode 100644 index 0000000..522f307 --- /dev/null +++ b/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>( + 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>( + (errors) => { + console.log(errors) + }, + [], + ); + + const formProps = useForm({ + 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( + + + { + + + + } + + + + + + + + ) +} + +export default CreateCompany; \ No newline at end of file diff --git a/src/components/CreateCompany/CreateCompanyWrapper.tsx b/src/components/CreateCompany/CreateCompanyWrapper.tsx new file mode 100644 index 0000000..1becfc6 --- /dev/null +++ b/src/components/CreateCompany/CreateCompanyWrapper.tsx @@ -0,0 +1,10 @@ +import CreateCompany from "./CreateCompany"; + +const CreateCompanyWrapper: React.FC = async () => { + return ( + + ) +} + +export default CreateCompanyWrapper; \ No newline at end of file diff --git a/src/components/CreateCompany/index.ts b/src/components/CreateCompany/index.ts new file mode 100644 index 0000000..222e8c7 --- /dev/null +++ b/src/components/CreateCompany/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateCompanyWrapper" \ No newline at end of file diff --git a/src/components/CreateStaff/CreateStaff.tsx b/src/components/CreateStaff/CreateStaff.tsx index 0aed5ab..35b8ded 100644 --- a/src/components/CreateStaff/CreateStaff.tsx +++ b/src/components/CreateStaff/CreateStaff.tsx @@ -179,7 +179,7 @@ const CreateStaff: React.FC = ({ Title }) => { label: t("Team"), type: "combo-Obj", options: teamCombo, - required: true, + required: false, }, { id: "departmentId", @@ -193,14 +193,14 @@ const CreateStaff: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ Title }) => { label: t("Emergency Contact Phone"), type: "text", value: "", - pattern: "^\\d{8}$", + // pattern: "^\\d{8}$", message: t("input correct phone no."), required: true, }, diff --git a/src/components/CustomInputForm/CustomInputForm.tsx b/src/components/CustomInputForm/CustomInputForm.tsx index ffcaa7a..dc7f269 100644 --- a/src/components/CustomInputForm/CustomInputForm.tsx +++ b/src/components/CustomInputForm/CustomInputForm.tsx @@ -274,7 +274,7 @@ const CustomInputForm: React.FC = ({ 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} diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerDetail/ContactInfo.tsx index 9566264..f608b00 100644 --- a/src/components/CustomerDetail/ContactInfo.tsx +++ b/src/components/CustomerDetail/ContactInfo.tsx @@ -231,7 +231,7 @@ const ContactInfo: React.FC = ({ 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 { diff --git a/src/components/CustomerDetail/CustomerDetail.tsx b/src/components/CustomerDetail/CustomerDetail.tsx index 2c81e17..88a99ad 100644 --- a/src/components/CustomerDetail/CustomerDetail.tsx +++ b/src/components/CustomerDetail/CustomerDetail.tsx @@ -167,7 +167,7 @@ const CustomerDetail: React.FC = ({ 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 = ({ 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" }) } diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerDetail/CustomerInfo.tsx index 695f6d5..3eaa327 100644 --- a/src/components/CustomerDetail/CustomerInfo.tsx +++ b/src/components/CustomerDetail/CustomerInfo.tsx @@ -107,7 +107,7 @@ const CustomerInfo: React.FC = ({ 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")} diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index 02a3d04..1fbf96b 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/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 { diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx new file mode 100644 index 0000000..56b6204 --- /dev/null +++ b/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>; +type SearchParamNames = keyof SearchQuery; + +const InvoiceSearch: React.FC = ({ invoices }) => { + const { t } = useTranslation("invoices"); + + const [filteredInvoices, setFilteredInvoices] = useState(invoices); + + const searchCriteria: Criterion[] = 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[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onProjectClick, + buttonIcon: , + }, + { 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 ( + <> + { + 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} + /> + + items={filteredInvoices} + columns={columns} + /> + + ); +}; + +export default InvoiceSearch; diff --git a/src/components/InvoiceSearch/InvoiceSearchLoading.tsx b/src/components/InvoiceSearch/InvoiceSearchLoading.tsx new file mode 100644 index 0000000..927c6c6 --- /dev/null +++ b/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 ( + <> + + + + + + + + + + + Invoice + + + + + + + + + + + ); +}; + +export default InvoiceSearchLoading; diff --git a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx new file mode 100644 index 0000000..4312fb9 --- /dev/null +++ b/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 ; +}; + +InvoiceSearchWrapper.Loading = InvoiceSearchLoading; + +export default InvoiceSearchWrapper; diff --git a/src/components/InvoiceSearch/index.ts b/src/components/InvoiceSearch/index.ts new file mode 100644 index 0000000..14315af --- /dev/null +++ b/src/components/InvoiceSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./InvoiceSearchWrapper"; diff --git a/src/components/SalarySearch/SalarySearch.tsx b/src/components/SalarySearch/SalarySearch.tsx index 8513fca..e7469fd 100644 --- a/src/components/SalarySearch/SalarySearch.tsx +++ b/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 = ({ 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 = ({ 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)) ), ); diff --git a/src/components/SalarySearch/SalarySearchWrapper.tsx b/src/components/SalarySearch/SalarySearchWrapper.tsx index 4687c17..3c910f9 100644 --- a/src/components/SalarySearch/SalarySearchWrapper.tsx +++ b/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 ; + return ; }; SalarySearchWrapper.Loading = SalarySearchLoading; diff --git a/src/components/SubsidiaryDetail/ContactInfo.tsx b/src/components/SubsidiaryDetail/ContactInfo.tsx index 9e2c16d..ef84ad7 100644 --- a/src/components/SubsidiaryDetail/ContactInfo.tsx +++ b/src/components/SubsidiaryDetail/ContactInfo.tsx @@ -231,7 +231,7 @@ const ContactInfo: React.FC = ({ 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" }) diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx index 339fd52..5f89570 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryDetail.tsx @@ -157,7 +157,7 @@ const SubsidiaryDetail: React.FC = ({ 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" }) } diff --git a/src/i18n/en/breadcrumb.json b/src/i18n/en/breadcrumb.json index 72bdcc8..5361966 100644 --- a/src/i18n/en/breadcrumb.json +++ b/src/i18n/en/breadcrumb.json @@ -1,6 +1,6 @@ { "Overview": "Overview", - "customer": "Customer", - "Create Customer": "Create Customer" + "customer": "Client", + "Create Customer": "Create Client" } \ No newline at end of file diff --git a/src/i18n/en/salarys.json b/src/i18n/en/salarys.json new file mode 100644 index 0000000..18a38ff --- /dev/null +++ b/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)" +} \ No newline at end of file diff --git a/src/i18n/en/subsidiary.json b/src/i18n/en/subsidiary.json index d26d4e1..3c06c1c 100644 --- a/src/i18n/en/subsidiary.json +++ b/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", diff --git a/src/i18n/zh/salarys.json b/src/i18n/zh/salarys.json new file mode 100644 index 0000000..d28eb81 --- /dev/null +++ b/src/i18n/zh/salarys.json @@ -0,0 +1,7 @@ +{ + "Details": "詳請", + "Salary Point": "薪點", + "Lower Limit": "薪金下限(港元)", + "Upper Limit": "薪金上限(港元)", + "Hourly Rate": "時薪(港元)" +} \ No newline at end of file