From 494ac814b864d7ee0b704f7f768f57529a70febd Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 24 Apr 2024 16:12:09 +0800 Subject: [PATCH] update team master --- src/app/(main)/settings/team/create/page.tsx | 45 ++++ src/app/(main)/settings/team/page.tsx | 53 ++++ src/app/api/team/index.ts | 20 ++ src/components/CreateTeam/CreateTeam.tsx | 126 ++++++++++ .../CreateTeam/CreateTeamLoading.tsx | 40 +++ .../CreateTeam/CreateTeamWrapper.tsx | 26 ++ src/components/CreateTeam/StaffAllocation.tsx | 233 ++++++++++++++++++ src/components/CreateTeam/TeamInfo.tsx | 69 ++++++ src/components/CreateTeam/index.ts | 1 + src/components/TeamSearch/TeamSearch.tsx | 90 +++++++ .../TeamSearch/TeamSearchLoading.tsx | 40 +++ .../TeamSearch/TeamSearchWrapper.tsx | 21 ++ src/components/TeamSearch/index.ts | 1 + 13 files changed, 765 insertions(+) create mode 100644 src/app/(main)/settings/team/create/page.tsx create mode 100644 src/app/(main)/settings/team/page.tsx create mode 100644 src/app/api/team/index.ts create mode 100644 src/components/CreateTeam/CreateTeam.tsx create mode 100644 src/components/CreateTeam/CreateTeamLoading.tsx create mode 100644 src/components/CreateTeam/CreateTeamWrapper.tsx create mode 100644 src/components/CreateTeam/StaffAllocation.tsx create mode 100644 src/components/CreateTeam/TeamInfo.tsx create mode 100644 src/components/CreateTeam/index.ts create mode 100644 src/components/TeamSearch/TeamSearch.tsx create mode 100644 src/components/TeamSearch/TeamSearchLoading.tsx create mode 100644 src/components/TeamSearch/TeamSearchWrapper.tsx create mode 100644 src/components/TeamSearch/index.ts diff --git a/src/app/(main)/settings/team/create/page.tsx b/src/app/(main)/settings/team/create/page.tsx new file mode 100644 index 0000000..721fda7 --- /dev/null +++ b/src/app/(main)/settings/team/create/page.tsx @@ -0,0 +1,45 @@ +// 'use client'; +import { I18nProvider, getServerI18n } from "@/i18n"; +import CustomInputForm from "@/components/CustomInputForm"; +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 Tab from "@mui/material/Tab"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Task, TaskTemplate } from "@/app/api/tasks"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; +import { Error } from "@mui/icons-material"; +import { ProjectCategory } from "@/app/api/projects"; +import { Grid, Typography } from "@mui/material"; +import CreateStaffForm from "@/components/CreateStaff/CreateStaff"; +import CreateTeam from "@/components/CreateTeam"; + +const CreateTeamPage: React.FC = async () => { + const { t } = await getServerI18n("team"); + + const title = ['', t('Additional Info')] + // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") + // console.log(regex) + + return ( + <> + {t("Create Team")} + + + + + ); +}; + +export default CreateTeamPage; diff --git a/src/app/(main)/settings/team/page.tsx b/src/app/(main)/settings/team/page.tsx new file mode 100644 index 0000000..5e78fb3 --- /dev/null +++ b/src/app/(main)/settings/team/page.tsx @@ -0,0 +1,53 @@ +import { preloadClaims } from "@/app/api/claims"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import StaffSearch from "@/components/StaffSearch"; +import TeamSearch from "@/components/TeamSearch"; +import { I18nProvider, 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: "Team", + }; + + + const Team: React.FC = async () => { + const { t } = await getServerI18n("Team"); + // preloadTeamLeads(); + // preloadStaff(); + return ( + <> + + + {t("Team")} + + + + + }> + + + + + ); + }; + + export default Team; \ No newline at end of file diff --git a/src/app/api/team/index.ts b/src/app/api/team/index.ts new file mode 100644 index 0000000..81799d7 --- /dev/null +++ b/src/app/api/team/index.ts @@ -0,0 +1,20 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + + +export interface TeamResult { + action: any; + id: number; + name: string; + code: string; + description: string; + } + + +export const fetchTeam = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/team`, { + next: { tags: ["team"] }, + }); + }); \ No newline at end of file diff --git a/src/components/CreateTeam/CreateTeam.tsx b/src/components/CreateTeam/CreateTeam.tsx new file mode 100644 index 0000000..93b585e --- /dev/null +++ b/src/components/CreateTeam/CreateTeam.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import StaffAllocation from "./StaffAllocation"; +import { StaffResult } from "@/app/api/staff"; +import { CreateTeamInputs, saveTeam } from "@/app/api/team/actions"; +import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; +import { Check, Close } from "@mui/icons-material"; +import { useCallback, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { Error } from "@mui/icons-material"; +import TeamInfo from "./TeamInfo"; + +export interface Props { + allstaff: StaffResult[]; +} + +const CreateTeam: React.FC = ({ allstaff }) => { + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const [tabIndex, setTabIndex] = useState(0); + const { t } = useTranslation(); + const searchParams = useSearchParams() + + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + await saveTeam(data); + router.replace("/settings/team"); + } catch (e) { + console.log(e); + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router] + ); + + const handleCancel = () => { + router.back(); + }; + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], +); +const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors, +) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } +}; + return ( + <> + + + + + ) : undefined + } + iconPosition="end" + /> + + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + {tabIndex === 1 && } + + {/* */} + + + + + + + + ); +}; + +export default CreateTeam; diff --git a/src/components/CreateTeam/CreateTeamLoading.tsx b/src/components/CreateTeam/CreateTeamLoading.tsx new file mode 100644 index 0000000..48c009d --- /dev/null +++ b/src/components/CreateTeam/CreateTeamLoading.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 CreateTeamLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + CreateTeam + + + + + + + + + + + ); +}; + +export default CreateTeamLoading; diff --git a/src/components/CreateTeam/CreateTeamWrapper.tsx b/src/components/CreateTeam/CreateTeamWrapper.tsx new file mode 100644 index 0000000..18b41a5 --- /dev/null +++ b/src/components/CreateTeam/CreateTeamWrapper.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import CreateTeam from "./CreateTeam"; +import CreateTeamLoading from "./CreateTeamLoading"; +// import { fetchTeam, fetchTeamLeads } from "@/app/api/team"; +import { useSearchParams } from "next/navigation"; +import { fetchStaffCombo } from "@/app/api/staff/actions"; +import { fetchStaff } from "@/app/api/staff"; + +interface SubComponents { + Loading: typeof CreateTeamLoading; +} + +const CreateTeamWrapper: React.FC & SubComponents = async () => { + + const [ + staff, + ] = await Promise.all([ + fetchStaff(), + ]); + + return ; +}; + +CreateTeamWrapper.Loading = CreateTeamLoading; + +export default CreateTeamWrapper; diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx new file mode 100644 index 0000000..f85c89b --- /dev/null +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -0,0 +1,233 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import CustomInputForm from "../CustomInputForm"; +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import CreateTeamForm from "../CreateTeamForm"; +import { CreateTeamInputs } from "@/app/api/team/actions"; +import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions"; +import { StaffResult, StaffTeamTable } from "@/app/api/staff"; +import SearchResults, { Column } from "../SearchResults"; +import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; +import { Card } from "reactstrap"; +import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material"; +import { differenceBy } from "lodash"; +import StarsIcon from '@mui/icons-material/Stars'; + +export interface Props { + allStaffs: StaffResult[]; +} + +const StaffAllocation: React.FC = ({ allStaffs: staff }) => { + const { t } = useTranslation(); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + + const initialStaffs = staff.map((s) => ({ ...s })); +// console.log(initialStaffs) + const [filteredStaff, setFilteredStaff] = useState(initialStaffs); + const [selectedStaff, setSelectedStaff] = useState( + initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) + ); + const [seletedTeamLead, setSeletedTeamLead] = useState() + // Adding / Removing staff + + const addStaff = useCallback((staff: StaffResult) => { + setSelectedStaff((s) => [...s, staff]); + }, []); + + const removeStaff = useCallback((staff: StaffResult) => { + setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); + }, []); + + const setTeamLead = useCallback((staff: StaffResult) => { + setSeletedTeamLead(staff.id) + const rearrangedList = getValues("addStaffIds").reduce((acc, num, index) => { + if (num === staff.id && index !== 0) { + acc.splice(index, 1); + acc.unshift(num); + } + return acc; + }, getValues("addStaffIds")); + console.log(rearrangedList) + console.log(selectedStaff) + + const rearrangedStaff = rearrangedList.map((id) => { + return selectedStaff.find((staff) => staff.id === id); + }); + console.log(rearrangedStaff) + // setSelectedStaff(rearrangedStaff as StaffResult[]); + + setValue("addStaffIds", rearrangedList) + }, []); + + const clearSubsidiary = useCallback(() => { + if (defaultValues !== undefined) { + resetField("addStaffIds"); + setSelectedStaff( + initialStaffs.filter((s) => defaultValues.addStaffIds?.includes(s.id)) + ); + } + }, [defaultValues]); + + // Sync with form + useEffect(() => { + console.log(selectedStaff) + setValue( + "addStaffIds", + selectedStaff.map((s) => s.id) + ); + }, [selectedStaff, setValue]); + + const StaffPoolColumns = useMemo[]>( + () => [ + { + label: t("Add"), + name: "id", + onClick: addStaff, + buttonIcon: , + }, + { label: t("Staff Id"), name: "staffId" }, + { label: t("Staff Name"), name: "name" }, + { label: t("Current Position"), name: "currentPosition" }, + ], + [addStaff, t] + ); + + const allocatedStaffColumns = useMemo[]>( + () => [ + { + label: t("Remove"), + name: "action", + onClick: removeStaff, + buttonIcon: , + }, + { label: t("Staff Id"), name: "staffId" }, + { label: t("Staff Name"), name: "name" }, + { label: t("Current Position"), name: "currentPosition" }, + { + label: t("Team Lead"), + name: "action", + onClick: setTeamLead, + buttonIcon: , + }, + ], + [removeStaff, t] + ); + + const [query, setQuery] = React.useState(""); + const onQueryInputChange = React.useCallback< + React.ChangeEventHandler + >((e) => { + setQuery(e.target.value); + }, []); + const clearQueryInput = React.useCallback(() => { + setQuery(""); + }, []); + + React.useEffect(() => { + // setFilteredStaff( + // initialStaffs.filter((s) => { + // const q = query.toLowerCase(); + // // s.staffId.toLowerCase().includes(q) + // // const q = query.toLowerCase(); + // // return s.name.toLowerCase().includes(q); + // // s.code.toString().includes(q) || + // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) + // }) + // ); + }, [staff, query]); + + const resetStaff = React.useCallback(() => { + clearQueryInput(); + clearSubsidiary(); + }, [clearQueryInput, clearSubsidiary]); + + const formProps = useForm({ + }); + + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], + ); + + return ( + <> + + + + + + {t("staff")} + + + + + + + + + + ), + }} + /> + + + + + + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + + + + + + + ); +}; + +export default StaffAllocation; diff --git a/src/components/CreateTeam/TeamInfo.tsx b/src/components/CreateTeam/TeamInfo.tsx new file mode 100644 index 0000000..9dd4060 --- /dev/null +++ b/src/components/CreateTeam/TeamInfo.tsx @@ -0,0 +1,69 @@ +"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 Grid from "@mui/material/Grid"; +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, useFormContext } from "react-hook-form"; +import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import { useCallback } from "react"; +import { CreateTeamInputs } from "@/app/api/team/actions"; + +const TeamInfo: React.FC = ( + { + // customerTypes, + } +) => { + const { t } = useTranslation(); + const { + register, + formState: { errors, defaultValues }, + control, + reset, + resetField, + setValue, + } = useFormContext(); + + const resetCustomer = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("description"); + } + }, [defaultValues]); + + return ( + <> + + + + + {t("Team Info")} + + + + + + + + + + + ); +}; +export default TeamInfo; diff --git a/src/components/CreateTeam/index.ts b/src/components/CreateTeam/index.ts new file mode 100644 index 0000000..0dc474f --- /dev/null +++ b/src/components/CreateTeam/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateTeamWrapper"; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx new file mode 100644 index 0000000..85d970d --- /dev/null +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { TeamResult } from "@/app/api/team"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from '@mui/icons-material/Delete'; +import { deleteStaff } from "@/app/api/staff/actions"; +import { useRouter } from "next/navigation"; + +interface Props { + team: TeamResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const TeamSearch: React.FC = ({ team }) => { + const { t } = useTranslation(); + const [filteredTeam, setFilteredTeam] = useState(team); + const [data, setData] = useState(); + const [isOpen, setIsOpen] = useState(false); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team Name"), + paramName: "name", + type: "text", + }, + { + label: t("Team Code"), + paramName: "code", + type: "text", + }, + { + label: t("Team Description"), + paramName: "description", + type: "text", + }, + ], + [t], + ); + + const columns = useMemo[]>( + () => [ + // { + // name: "action", + // label: t("Actions"), + // onClick: onStaffClick, + // buttonIcon: , + // }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("description") }, + // { + // name: "action", + // label: t("Actions"), + // onClick: deleteClick, + // buttonIcon: , + // }, + ], + [t], + ); + + return ( + <> + { + // setFilteredStaff( + // staff.filter( + // (s) => + // s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && + // s.name.toLowerCase().includes(query.name.toLowerCase()) + // // (query.team === "All" || s.team === query.team) && + // // (query.category === "All" || s.category === query.category) && + // // (query.team === "All" || s.team === query.team), + // ) + // ) + }} + /> + items={filteredTeam} columns={columns} /> + + + ); +}; +export default TeamSearch; diff --git a/src/components/TeamSearch/TeamSearchLoading.tsx b/src/components/TeamSearch/TeamSearchLoading.tsx new file mode 100644 index 0000000..2d9be5b --- /dev/null +++ b/src/components/TeamSearch/TeamSearchLoading.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 TeamSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TeamSearchLoading; diff --git a/src/components/TeamSearch/TeamSearchWrapper.tsx b/src/components/TeamSearch/TeamSearchWrapper.tsx new file mode 100644 index 0000000..bd4f714 --- /dev/null +++ b/src/components/TeamSearch/TeamSearchWrapper.tsx @@ -0,0 +1,21 @@ +// import { fetchTeam, fetchTeamLeads } from "@/app/api/Team"; +import React from "react"; +import TeamSearch from "./TeamSearch"; +import TeamSearchLoading from "./TeamSearchLoading"; +import { fetchTeam } from "@/app/api/team"; +// import { preloadTeam } from "@/app/api/Team"; + +interface SubComponents { + Loading: typeof TeamSearchLoading; +} + +const TeamSearchWrapper: React.FC & SubComponents = async () => { + const Team = await fetchTeam(); + console.log(Team); + + return ; +}; + +TeamSearchWrapper.Loading = TeamSearchLoading; + +export default TeamSearchWrapper; diff --git a/src/components/TeamSearch/index.ts b/src/components/TeamSearch/index.ts new file mode 100644 index 0000000..f0bf2b6 --- /dev/null +++ b/src/components/TeamSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./TeamSearchWrapper";