diff --git a/src/app/(main)/invoice/page.tsx b/src/app/(main)/invoice/page.tsx
index 1806d2e..d9d18bf 100644
--- a/src/app/(main)/invoice/page.tsx
+++ b/src/app/(main)/invoice/page.tsx
@@ -5,6 +5,8 @@ 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",
@@ -33,6 +35,9 @@ const Invoice: React.FC = async () => {
{t("Create Invoice")}
+ }>
+
+
>
)
};
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 b0e6db0..f342177 100644
--- a/src/app/api/companys/actions.ts
+++ b/src/app/api/companys/actions.ts
@@ -2,6 +2,7 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
+import { Dayjs } from "dayjs";
import { cache } from "react";
export interface comboProp {
@@ -14,19 +15,19 @@ export interface combo {
}
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;
- distract: String;
- email: String;
+ 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) => {
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/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/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
index f3268c8..ccef906 100644
--- a/src/components/CreateCompany/CompanyDetails.tsx
+++ b/src/components/CreateCompany/CompanyDetails.tsx
@@ -15,8 +15,10 @@ import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
-import { Controller, useFormContext } from "react-hook-form";
+import { 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 = ({
}) => {
@@ -25,8 +27,11 @@ const CompanyDetails: React.FC = ({
register,
formState: { errors },
control,
+ setValue,
+ getValues,
} = useFormContext();
+
return (
@@ -95,6 +100,116 @@ const CompanyDetails: React.FC = ({
error={Boolean(errors.email)}
/>
+
+ {
+ 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%' }}
+ />
+ );
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
{/*
diff --git a/src/components/CreateCompany/CreateCompany.tsx b/src/components/CreateCompany/CreateCompany.tsx
index 4394f51..522f307 100644
--- a/src/components/CreateCompany/CreateCompany.tsx
+++ b/src/components/CreateCompany/CreateCompany.tsx
@@ -16,6 +16,9 @@ import {
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 = ({
@@ -35,7 +38,7 @@ const CreateCompany: React.FC = ({
setServerError("");
// console.log(JSON.stringify(data));
await saveCompany(data)
- router.replace("/settings/companys");
+ router.replace("/settings/company");
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
@@ -59,11 +62,11 @@ const CreateCompany: React.FC = ({
phone: "",
otHourTo: "",
otHourFrom: "",
- normalHourTo: "",
- normalHourFrom: "",
+ normalHourTo: dayjs().format('HH:mm:ss'),
+ normalHourFrom: dayjs().format('HH:mm:ss'),
currency: "",
address: "",
- distract: "",
+ district: "",
email: "",
},
});
@@ -78,7 +81,9 @@ const CreateCompany: React.FC = ({
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
{
-
+
+
+
}
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/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/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