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")}
+
+ }
+ LinkComponent={Link}
+ href="/invoice/new"
+ >
+ {t("Create 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")}
- }
LinkComponent={Link}
href="/settings/position/new"
>
{t("Create 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%' }}
+ />
+ );
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ }>
+ {t("Reset")}
+
+ */}
+
+
+ );
+};
+
+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(
+
+
+ {
+
+
+
+ }
+
+
+ }
+ onClick={handleCancel}
+ >
+ {t("Cancel")}
+
+ } type="submit">
+ {t("Confirm")}
+
+
+
+
+ )
+}
+
+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