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")}
+
+ }
+ LinkComponent={Link}
+ href="/settings/team/create"
+ >
+ {t("Create 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 && }
+
+ {/* */}
+
+ }
+ onClick={handleCancel}
+ >
+ {t("Cancel")}
+
+ }
+ type="submit"
+ // disabled={Boolean(formProps.watch("isGridEditing"))}
+ >
+ {t("Confirm")}
+
+
+
+
+ >
+ );
+};
+
+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";