diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx index c737430..fef65c1 100644 --- a/src/app/(main)/projects/create/page.tsx +++ b/src/app/(main)/projects/create/page.tsx @@ -1,3 +1,4 @@ +"use client"; import { fetchProjectCategories } from "@/app/api/projects"; import { preloadStaff } from "@/app/api/staff"; import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; diff --git a/src/app/(main)/staff/create/page.tsx b/src/app/(main)/staff/create/page.tsx index b2d6bc6..08bb1eb 100644 --- a/src/app/(main)/staff/create/page.tsx +++ b/src/app/(main)/staff/create/page.tsx @@ -60,42 +60,117 @@ const CreateStaff: React.FC = async () => { { id: "name", label: t("Staff Name"), - type: "textfield", + type: "text", value: "asdasd", // required: "asdasd", // option: "asdasd", }, { - id: "date", - label: "date test", - type: "multiDate", - value: "asdasd", + id: "currentPosition", + label: t("Current Position"), + type: "combo-Obj", + // value: "asdasd", // required: "asdasd", - // option: "asdasd", + options: [{id: 1, key: 1, value: 1, label: "Potato"}, {id: 2, key: 2, value: 2, label: "Tomato"}], }, { - id: "date2", - label: "combo test", + id: "joinPosition", + label: t("Join Position"), type: "combo-Obj", - value: "asdasd", + // value: "asdasd", // required: "asdasd", - options: [{id: 1, key: 1, value: 1, label: "first"}, {id: 2, key: 1, value: 2, label: "second"}], + options: [{id: 1, key: 1, value: 1, label: "Potato"}, {id: 2, key: 2, value: 2, label: "Tomato"}], }, { - id: "field1", - label: "remarks test", + id: "companyId", + label: t("Company"), + type: "combo-Obj", + // value: "asdasd", + // required: "asdasd", + options: [{id: 1, key: 1, value: 1, label: "Company A"}, {id: 2, key: 2, value: 2, label: "Company B"}], + }, + { + id: "gradeId", + label: t("Grade"), + type: "combo-Obj", + options: [{id: 1, key: 1, value: 1, label: "A"}, {id: 2, key: 2, value: 2, label: "B"}], + }, + { + id: "teamId", + label: t("Team"), + type: "combo-Obj", + options: [{id: 1, key: 1, value: 1, label: "A"}, {id: 2, key: 2, value: 2, label: "B"}], + }, + { + id: "salaryEffId", + label: t("Salary point ID with effective date"), + type: "combo-Obj", + options: [{id: 1, key: 1, value: 1, label: t("first")}, {id: 2, key: 2, value: 2, label: t("second")}], + }, + { + id: "hourlyRate", + label: t("Hourly Rate"), + type: "numeric", + value: "", + }, + { + id: "email", + label: t("Email"), + type: "text", + value: "", + }, + { + id: "phone1", + label: t("Phone1"), + type: "numeric", + value: "", + }, + { + id: "phone2", + label: t("Phone2"), + type: "numeric", + value: "", + }, + { + id: "emergContactName", + label: t("Emergency Contact Name"), + type: "text", + value: "", + }, + { + id: "emergContactPhone", + label: t("Emergency Contact Phone"), + type: "numeric", + value: "", + }, + { + id: "employType", + label: t("Employ Type"), + type: "combo-Obj", + options: [{id: 1, key: "FT", value: "FT", label: t("FT")}, {id: 2, key: "PT", value: "PT", label: t("PT")}], + value: "", + }, + { + id: "departDate", + label: t("Depart Date"), + type: "multiDate", + value: "", + }, + { + id: "departReason", + label: t("Depart Reason"), + type: "text", + value: "", + }, + { + id: "remark", + label: t("Remark"), type: "remarks", value: "", - // required: "asdasd", - // options: "asdasd", }, ], ]; - const handleSubmit = (data: any) => { - console.log(data); - }; - return ( <> {t("Create Staff")} diff --git a/src/app/(main)/staff/page.tsx b/src/app/(main)/staff/page.tsx new file mode 100644 index 0000000..13804ce --- /dev/null +++ b/src/app/(main)/staff/page.tsx @@ -0,0 +1,48 @@ +// 'use client'; +import { preloadClaims } from "@/app/api/claims"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import StaffSearch from "@/components/StaffSearch"; +import { getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Staff", +}; + +const Staff: React.FC = async () => { + const { t } = await getServerI18n("projects"); + preloadTeamLeads() + return ( + <> + + + {t("Staff")} + + + + }> + + + + ); +}; + +export default Staff; diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts new file mode 100644 index 0000000..0547d66 --- /dev/null +++ b/src/app/api/staff/actions.ts @@ -0,0 +1,39 @@ +"use server"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; + +export interface CreateCustomInputs { + // Project details + projectCode: string; + projectName: string; + + // Miscellaneous + expectedProjectFee: string; + } +export interface CreateStaffInputs { + name: string; + currentPositionId: number; + joinPositionId: number; + companyId: number; + gradeId: number; + teamId: number; + salaryEffId: number; + email: string; + phone1: string; + phone2: string; + emergContactName: string; + emergContactPhone: string; + employType: string; + departDate: string; + departReason: string; + remark: string; + } + + +export const saveStaff = async (data: CreateStaffInputs) => { + return serverFetchJson(`${BASE_API_URL}/staffs/new`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/staff/index.ts b/src/app/api/staff/index.ts index 617169b..7a1283c 100644 --- a/src/app/api/staff/index.ts +++ b/src/app/api/staff/index.ts @@ -13,8 +13,36 @@ export interface Staff { }; } -export const preloadStaff = () => { +export interface resultObj { +records: StaffResult[] +} +export interface StaffResult { + id: number; + created: string; + name: string; + cost: number; + staffId: string; + type: string; + currPos: string; + joinPos: string; + companyId: string; + skillSetId: number; + departmentId: number; + phone1: string; + phone2: string; + email: string; + emergContactName: string; + emergContactPhone: string; + employType: string; + departDate: string; + departReason: string; + remarks: string; +} + + +export const preloadTeamLeads = () => { fetchTeamLeads(); + fetchStaff(); }; export const fetchTeamLeads = cache(async () => { @@ -22,3 +50,14 @@ export const fetchTeamLeads = cache(async () => { next: { tags: ["teamLeads"] }, }); }); + +export const preloadStaff = () => { + fetchStaff(); +}; + +export const fetchStaff = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/staffs/newlist`, { + next: { tags: ["teamLeads"] }, + }); +}); + diff --git a/src/components/CreateStaff/CreateStaffForm.tsx b/src/components/CreateStaff/CreateStaffForm.tsx index e73cf1d..a44b5b2 100644 --- a/src/components/CreateStaff/CreateStaffForm.tsx +++ b/src/components/CreateStaff/CreateStaffForm.tsx @@ -1,6 +1,17 @@ -'use client' -import { useState } from 'react'; -import CustomInputForm from '../CustomInputForm'; +"use client"; +import { useCallback, useState } from "react"; +import CustomInputForm from "../CustomInputForm"; +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { CreateStaffInputs, saveStaff } from "@/app/api/staff/actions"; +import { Typography } from "@mui/material"; interface Field { // subtitle: string; @@ -21,25 +32,48 @@ interface formProps { fieldLists: Field[][]; } -const CreateStaffForm: React.FC = ({ - fieldLists -}) => { +const CreateStaffForm: React.FC = ({ fieldLists }) => { // const [formData, setFormData] = useState(null); + const router = useRouter(); + const { t } = useTranslation(); + const [serverError, setServerError] = useState(""); - const handleSubmit = (data: any) => { - console.log(data); - // Handle the form submission logic here - // setFormData(data); + // const handleSubmit = (data: any) => { + // console.log(data); + // // Handle the form submission logic here + // // setFormData(data); + // }; + const handleCancel = () => { + router.back(); }; + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + setServerError(""); + await saveStaff(data); + } catch (e) { + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router, t] + ); return ( - + <> + {serverError && ( + + {serverError} + + )} + + ); }; -export default CreateStaffForm; \ No newline at end of file +export default CreateStaffForm; diff --git a/src/components/CustomInputForm/CustomInputForm.tsx b/src/components/CustomInputForm/CustomInputForm.tsx index dc7f962..1e67f60 100644 --- a/src/components/CustomInputForm/CustomInputForm.tsx +++ b/src/components/CustomInputForm/CustomInputForm.tsx @@ -28,7 +28,7 @@ import { DemoItem } from "@mui/x-date-pickers/internals/demo"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import dayjs from "dayjs"; import { useEffect, useState } from "react"; -import { Check } from "@mui/icons-material"; +import { Check, Close } from "@mui/icons-material"; // interface Option { // // Define properties of each option object @@ -47,10 +47,13 @@ interface Field { required?: boolean; options?: any[]; readOnly?: boolean; + size?: number; + setValue?: any[]; } interface CustomInputFormProps { onSubmit: (data: any) => void; + onCancel: () => void; // resetForm: () => void; Title?: string[]; isActive: boolean; @@ -62,6 +65,7 @@ const CustomInputForm: React.FC = ({ isActive, fieldLists, onSubmit, + onCancel, // resetForm, }) => { const { t } = useTranslation(); @@ -130,6 +134,30 @@ const CustomInputForm: React.FC = ({ })); }; + const handleCancel = () => { + reset(); + // resetForm(); + // setFromDate(null); + setDateObj(null); + setValue({}); + if (onCancel) { + onCancel(); + } + // if fields include setValue func + // fieldLists.map((list) => { + // list.map((field) => { + // if (typeof field.setValue === 'function') { + // field.setValue(typeof field.value === 'boolean' ? false : null); + // } else if (typeof field.setValue === 'object') { + // field.setValue.map((setFunc: any) => { + // setFunc(null); + // }); + // } + // }) + // }); + // setToDate(null); + }; + fieldLists.forEach((list) => { list.forEach((obj) => { if ( @@ -168,9 +196,9 @@ const CustomInputForm: React.FC = ({ ) : null} {fieldList.map((field: Field) => { - if (field.type === "textfield") { + if (field.type === "text") { return ( - + = ({ ); } else if (field.type === "multiDate") { return ( - + { handleDateChange(field.id, newValue); }} @@ -202,7 +236,7 @@ const CustomInputForm: React.FC = ({ ); } else if (field.type === "combo-Obj") { return ( - + {field.label} @@ -259,7 +293,7 @@ const CustomInputForm: React.FC = ({ ); } else if (field.type === "numeric") { return ( - + = ({ ); } else if (field.type === "numeric-positive") { return ( - + = ({ ); } else if (field.type === "checkbox") { return ( - + = ({ ); } else { return ( - + = ({ ))} - {/* */} + diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 1c5b640..58c957d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -17,6 +17,7 @@ import Assignment from "@mui/icons-material/Assignment"; import Settings from "@mui/icons-material/Settings"; import Analytics from "@mui/icons-material/Analytics"; import Payments from "@mui/icons-material/Payments"; +import Staff from "@mui/icons-material/PeopleAlt"; import { useTranslation } from "react-i18next"; import Typography from "@mui/material/Typography"; import { usePathname } from "next/navigation"; @@ -87,6 +88,7 @@ const navigationItems: NavigationItem[] = [ { icon: , label: "Invoice", path: "/invoice" }, { icon: , label: "Analysis Report", path: "/analytics" }, { icon: , label: "Setting", path: "/settings" }, + { icon: , label: "Staff", path: "/staff" }, ]; const NavigationContent: React.FC = () => { diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx new file mode 100644 index 0000000..ec55280 --- /dev/null +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { StaffResult } from "@/app/api/staff"; +import React, { useCallback, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox/index"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; + +interface Props { + staff: StaffResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const StaffSearch: React.FC = ({ staff }) => { + const { t } = useTranslation("staff"); + + // If claim searching is done on the server-side, then no need for this. + const [filteredClaims, setFilteredClaims] = useState(staff); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff Name"), + paramName: "name", + type: "text" + }, + { + label: t("Cost (HKD)"), + paramName: "cost", + type: "text", + }, + { + label: t("Expense Type"), + paramName: "type", + type: "select", + options: ["Expense", "Petty Cash"], + }, + { + label: t("Current Position"), + paramName: "currPos", + type: "select", + options: ["Small Potato", "CEO"], + }, + { + label: t("Join Position"), + paramName: "joinPos", + type: "select", + options: ["Small Potato", "CEO"], + }, + { + label: t("Company"), + paramName: "companyId", + type: "select", + options: ["1", "2"], + }, + // { + // label: t("Skillset"), + // paramName: "skillSetId", + // type: "select", + // options: ["Fly", "Boxing"], + // }, + // { + // label: t("Department"), + // paramName: "departmentId", + // type: "select", + // options: ["Fly", "Boxing"], + // }, + // { + // label: t("phone1"), + // paramName: "phone1", + // type: "text", + // }, + // { + // label: t("phone2"), + // paramName: "phone2", + // type: "text", + // }, + // { + // label: t("email"), + // paramName: "email", + // type: "text", + // }, + // { + // label: t("Emergency Contact Name"), + // paramName: "emergContactName", + // type: "text", + // }, + // { + // label: t("Emergency Contact Phone"), + // paramName: "emergContactPhone", + // type: "text", + // }, + // { + // label: t("Employ Type"), + // paramName: "employType", + // type: "select", + // options: ["Full-time", "Part-time"], + // }, + // { + // label: t("Depart Date"), + // paramName: "departDate", + // type: "date", + // }, + // { + // label: t("Depart Reason"), + // paramName: "departReason", + // type: "text", + // }, + // { + // label: t("Remarks"), + // paramName: "remarks", + // type: "text", + // }, + ], + [t], + ); + + const onStaffClick = useCallback((staff: StaffResult) => { + console.log(staff); + }, []); + + const columns = useMemo[]>( + () => [ + // { + // name: "action", + // label: t("Actions"), + // onClick: onClaimClick, + // buttonIcon: , + // }, + { name: "created", label: t("Creation Date") }, + { name: "name", label: t("Related Project Name") }, + { name: "staffId", label: t("Staff Id") }, + { name: "type", label: t("Expense Type") }, + // { name: "status", label: t("Status") }, + { name: "remarks", label: t("Remarks") }, + ], + [t, onStaffClick], + ); + + return ( + <> + { + console.log(query); + }} + /> + items={filteredClaims} columns={columns} /> + + ); +}; + +export default StaffSearch; diff --git a/src/components/StaffSearch/StaffSearchLoading.tsx b/src/components/StaffSearch/StaffSearchLoading.tsx new file mode 100644 index 0000000..45c5c6d --- /dev/null +++ b/src/components/StaffSearch/StaffSearchLoading.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 StaffSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default StaffSearchLoading; diff --git a/src/components/StaffSearch/StaffSearchWrapper.tsx b/src/components/StaffSearch/StaffSearchWrapper.tsx new file mode 100644 index 0000000..5ffcd58 --- /dev/null +++ b/src/components/StaffSearch/StaffSearchWrapper.tsx @@ -0,0 +1,22 @@ +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import React from "react"; +import StaffSearch from "./StaffSearch"; +import StaffSearchLoading from "./StaffSearchLoading"; +// import { preloadStaff } from "@/app/api/staff"; + +interface SubComponents { + Loading: typeof StaffSearchLoading; +} + +const StaffSearchWrapper: React.FC & SubComponents = async () => { + const staff = await fetchStaff(); + // const try = ...staff + console.log(staff) + const records = staff.records; + + return ; +}; + +StaffSearchWrapper.Loading = StaffSearchLoading; + +export default StaffSearchWrapper; diff --git a/src/components/StaffSearch/index.ts b/src/components/StaffSearch/index.ts new file mode 100644 index 0000000..ca9e06a --- /dev/null +++ b/src/components/StaffSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./StaffSearchWrapper"; diff --git a/src/i18n/zh/createStaff.json b/src/i18n/zh/createStaff.json new file mode 100644 index 0000000..3dbc6a6 --- /dev/null +++ b/src/i18n/zh/createStaff.json @@ -0,0 +1,19 @@ +{ + "Staff Name": "Staff Name", + "Current Position": "Current Position", + "Join Position": "Join Position", + "Company": "Company", + "Grade": "Grade", + "Team": "Team", + "Salary point ID with effective date": "Salary point ID with effective date", + "Hourly Rate": "Hourly Rate", + "Email": "Email", + "Phone1": "Phone1", + "Phone2": "Phone2", + "Emergency Contact Name": "Emergency Contact Name", + "Emergency Contact Phone": "Emergency Contact Phone", + "Employ Type": "Employ Type", + "Depart Date": "Depart Date", + "Depart Reason": "Depart Reason", + "Remark": "Remark" + } \ No newline at end of file