From 3aeafee40217aca5bb6ada5543656cb5d13ad7b1 Mon Sep 17 00:00:00 2001 From: Wayne Date: Mon, 6 May 2024 18:41:05 +0900 Subject: [PATCH 1/2] Add leave input --- src/app/(main)/home/page.tsx | 12 +- src/app/api/projects/actions.ts | 4 +- src/app/api/projects/index.ts | 4 +- src/app/api/timesheets/actions.ts | 25 ++ src/app/api/timesheets/index.ts | 22 +- src/components/LeaveModal/LeaveModal.tsx | 127 ++++++++ src/components/LeaveModal/index.ts | 1 + src/components/LeaveTable/LeaveEntryTable.tsx | 283 ++++++++++++++++++ src/components/LeaveTable/LeaveTable.tsx | 133 ++++++++ src/components/LeaveTable/index.ts | 1 + .../TimesheetModal/TimesheetModal.tsx | 4 +- .../UserWorkspacePage/UserWorkspacePage.tsx | 73 +++-- .../UserWorkspaceWrapper.tsx | 14 +- 13 files changed, 653 insertions(+), 50 deletions(-) create mode 100644 src/components/LeaveModal/LeaveModal.tsx create mode 100644 src/components/LeaveModal/index.ts create mode 100644 src/components/LeaveTable/LeaveEntryTable.tsx create mode 100644 src/components/LeaveTable/LeaveTable.tsx create mode 100644 src/components/LeaveTable/index.ts diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index 155388f..2766580 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -1,9 +1,14 @@ import { Metadata } from "next"; import { I18nProvider } from "@/i18n"; import UserWorkspacePage from "@/components/UserWorkspacePage"; -import { fetchTimesheets } from "@/app/api/timesheets"; +import { + fetchLeaveTypes, + fetchLeaves, + fetchTimesheets, +} from "@/app/api/timesheets"; import { authOptions } from "@/config/authConfig"; import { getServerSession } from "next-auth"; +import { fetchAssignedProjects } from "@/app/api/projects"; export const metadata: Metadata = { title: "User Workspace", @@ -14,7 +19,10 @@ const Home: React.FC = async () => { // Get name for caching const username = session!.user!.name!; - await fetchTimesheets(username); + fetchTimesheets(username); + fetchAssignedProjects(username); + fetchLeaves(username); + fetchLeaveTypes(); return ( diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index 6bb5596..c80dff0 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -7,7 +7,7 @@ import { import { BASE_API_URL } from "@/config/api"; import { Task, TaskGroup } from "../tasks"; import { Customer } from "../customer"; -import { revalidateTag } from "next/cache"; +import { revalidatePath, revalidateTag } from "next/cache"; export interface CreateProjectInputs { // Project @@ -101,6 +101,6 @@ export const deleteProject = async (id: number) => { ); revalidateTag("projects"); - revalidateTag("assignedProjects"); + revalidatePath("/(main)/home"); return project; }; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 90b0e10..9cc4f01 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -138,11 +138,11 @@ export const fetchProjectWorkNatures = cache(async () => { }); }); -export const fetchAssignedProjects = cache(async () => { +export const fetchAssignedProjects = cache(async (username: string) => { return serverFetchJson( `${BASE_API_URL}/projects/assignedProjects`, { - next: { tags: ["assignedProjects"] }, + next: { tags: [`assignedProjects__${username}`] }, }, ); }); diff --git a/src/app/api/timesheets/actions.ts b/src/app/api/timesheets/actions.ts index 631c076..97b03a9 100644 --- a/src/app/api/timesheets/actions.ts +++ b/src/app/api/timesheets/actions.ts @@ -18,6 +18,16 @@ export interface RecordTimesheetInput { [date: string]: TimeEntry[]; } +export interface LeaveEntry { + id: number; + inputHours: number; + leaveTypeId: number; +} + +export interface RecordLeaveInput { + [date: string]: LeaveEntry[]; +} + export const saveTimesheet = async ( data: RecordTimesheetInput, username: string, @@ -35,3 +45,18 @@ export const saveTimesheet = async ( return savedRecords; }; + +export const saveLeave = async (data: RecordLeaveInput, username: string) => { + const savedRecords = await serverFetchJson( + `${BASE_API_URL}/timesheets/saveLeave`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + revalidateTag(`leaves_${username}`); + + return savedRecords; +}; diff --git a/src/app/api/timesheets/index.ts b/src/app/api/timesheets/index.ts index fd7d20d..d9b1862 100644 --- a/src/app/api/timesheets/index.ts +++ b/src/app/api/timesheets/index.ts @@ -1,10 +1,30 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; -import { RecordTimesheetInput } from "./actions"; +import { RecordLeaveInput, RecordTimesheetInput } from "./actions"; + +export interface LeaveType { + id: number; + name: string; +} export const fetchTimesheets = cache(async (username: string) => { return serverFetchJson(`${BASE_API_URL}/timesheets`, { next: { tags: [`timesheets_${username}`] }, }); }); + +export const fetchLeaves = cache(async (username: string) => { + return serverFetchJson( + `${BASE_API_URL}/timesheets/leaves`, + { + next: { tags: [`leaves_${username}`] }, + }, + ); +}); + +export const fetchLeaveTypes = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/timesheets/leaveTypes`, { + next: { tags: ["leaveTypes"] }, + }); +}); diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx new file mode 100644 index 0000000..b163003 --- /dev/null +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useMemo } from "react"; +import { + Box, + Button, + Card, + CardActions, + CardContent, + Modal, + SxProps, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Check, Close } from "@mui/icons-material"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { RecordLeaveInput, saveLeave } from "@/app/api/timesheets/actions"; +import dayjs from "dayjs"; +import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import LeaveTable from "../LeaveTable"; +import { LeaveType } from "@/app/api/timesheets"; + +interface Props { + isOpen: boolean; + onClose: () => void; + username: string; + defaultLeaveRecords?: RecordLeaveInput; + leaveTypes: LeaveType[]; +} + +const modalSx: SxProps = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: { xs: "calc(100% - 2rem)", sm: "90%" }, + maxHeight: "90%", + maxWidth: 1200, +}; + +const LeaveModal: React.FC = ({ + isOpen, + onClose, + username, + defaultLeaveRecords, + leaveTypes, +}) => { + const { t } = useTranslation("home"); + + const defaultValues = useMemo(() => { + const today = dayjs(); + return Array(7) + .fill(undefined) + .reduce((acc, _, index) => { + const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); + return { + ...acc, + [date]: defaultLeaveRecords?.[date] ?? [], + }; + }, {}); + }, [defaultLeaveRecords]); + + const formProps = useForm({ defaultValues }); + + const onSubmit = useCallback>( + async (data) => { + const savedRecords = await saveLeave(data, username); + + const today = dayjs(); + const newFormValues = Array(7) + .fill(undefined) + .reduce((acc, _, index) => { + const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); + return { + ...acc, + [date]: savedRecords[date] ?? [], + }; + }, {}); + + formProps.reset(newFormValues); + onClose(); + }, + [formProps, onClose, username], + ); + + const onCancel = useCallback(() => { + formProps.reset(defaultValues); + onClose(); + }, [defaultValues, formProps, onClose]); + + return ( + + + + + + {t("Record Leave")} + + + + + + + + + + + + + ); +}; + +export default LeaveModal; diff --git a/src/components/LeaveModal/index.ts b/src/components/LeaveModal/index.ts new file mode 100644 index 0000000..cd099c7 --- /dev/null +++ b/src/components/LeaveModal/index.ts @@ -0,0 +1 @@ +export { default } from "./LeaveModal"; diff --git a/src/components/LeaveTable/LeaveEntryTable.tsx b/src/components/LeaveTable/LeaveEntryTable.tsx new file mode 100644 index 0000000..9e9170d --- /dev/null +++ b/src/components/LeaveTable/LeaveEntryTable.tsx @@ -0,0 +1,283 @@ +import { Add, Check, Close, Delete } from "@mui/icons-material"; +import { Box, Button, Typography } from "@mui/material"; +import { + FooterPropsOverrides, + GridActionsCellItem, + GridColDef, + GridEventListener, + GridRowId, + GridRowModel, + GridRowModes, + GridRowModesModel, + GridToolbarContainer, + useGridApiRef, +} from "@mui/x-data-grid"; +import { useTranslation } from "react-i18next"; +import StyledDataGrid from "../StyledDataGrid"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { RecordLeaveInput, LeaveEntry } from "@/app/api/timesheets/actions"; +import { manhourFormatter } from "@/app/utils/formatUtil"; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween"; +import { LeaveType } from "@/app/api/timesheets"; + +dayjs.extend(isBetween); + +interface Props { + day: string; + leaveTypes: LeaveType[]; +} + +type LeaveEntryRow = Partial< + LeaveEntry & { + _isNew: boolean; + _error: string; + } +>; + +const EntryInputTable: React.FC = ({ day, leaveTypes }) => { + const { t } = useTranslation("home"); + + const { getValues, setValue } = useFormContext(); + const currentEntries = getValues(day); + + const [entries, setEntries] = useState(currentEntries || []); + + const [rowModesModel, setRowModesModel] = useState({}); + + const apiRef = useGridApiRef(); + const addRow = useCallback(() => { + const id = Date.now(); + setEntries((e) => [...e, { id, _isNew: true }]); + setRowModesModel((model) => ({ + ...model, + [id]: { mode: GridRowModes.Edit, fieldToFocus: "leaveTypeId" }, + })); + }, []); + + const validateRow = useCallback( + (id: GridRowId) => { + const row = apiRef.current.getRowWithUpdatedValues( + id, + "", + ) as LeaveEntryRow; + + // Test for errrors + let error: keyof LeaveEntry | "" = ""; + if (!row.leaveTypeId) { + error = "leaveTypeId"; + } else if (!row.inputHours || !(row.inputHours >= 0)) { + error = "inputHours"; + } + + apiRef.current.updateRows([{ id, _error: error }]); + return !error; + }, + [apiRef], + ); + + const handleCancel = useCallback( + (id: GridRowId) => () => { + setRowModesModel((model) => ({ + ...model, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + })); + const editedRow = entries.find((entry) => entry.id === id); + if (editedRow?._isNew) { + setEntries((es) => es.filter((e) => e.id !== id)); + } + }, + [entries], + ); + + const handleDelete = useCallback( + (id: GridRowId) => () => { + setEntries((es) => es.filter((e) => e.id !== id)); + }, + [], + ); + + const handleSave = useCallback( + (id: GridRowId) => () => { + if (validateRow(id)) { + setRowModesModel((model) => ({ + ...model, + [id]: { mode: GridRowModes.View }, + })); + } + }, + [validateRow], + ); + + const handleEditStop = useCallback>( + (params, event) => { + if (!validateRow(params.id)) { + event.defaultMuiPrevented = true; + } + }, + [validateRow], + ); + + const processRowUpdate = useCallback((newRow: GridRowModel) => { + const updatedRow = { ...newRow, _isNew: false }; + setEntries((es) => es.map((e) => (e.id === newRow.id ? updatedRow : e))); + return updatedRow; + }, []); + + const columns = useMemo( + () => [ + { + type: "actions", + field: "actions", + headerName: t("Actions"), + getActions: ({ id }) => { + if (rowModesModel[id]?.mode === GridRowModes.Edit) { + return [ + } + label={t("Save")} + onClick={handleSave(id)} + />, + } + label={t("Cancel")} + onClick={handleCancel(id)} + />, + ]; + } + + return [ + } + label={t("Remove")} + onClick={handleDelete(id)} + />, + ]; + }, + }, + { + field: "leaveTypeId", + headerName: t("Leave Type"), + width: 200, + editable: true, + type: "singleSelect", + valueOptions() { + return leaveTypes.map((p) => ({ value: p.id, label: p.name })); + }, + valueGetter({ value }) { + return value ?? ""; + }, + }, + { + field: "inputHours", + headerName: t("Hours"), + width: 100, + editable: true, + type: "number", + valueFormatter(params) { + return manhourFormatter.format(params.value); + }, + }, + ], + [t, rowModesModel, handleDelete, handleSave, handleCancel, leaveTypes], + ); + + useEffect(() => { + setValue(day, [ + ...entries + .filter( + (e) => + !e._isNew && !e._error && e.inputHours && e.leaveTypeId && e.id, + ) + .map((e) => ({ + id: e.id!, + inputHours: e.inputHours!, + leaveTypeId: e.leaveTypeId!, + })), + ]); + }, [getValues, entries, setValue, day]); + + const footer = ( + + + + ); + + return ( + { + let classname = ""; + if (params.row._error === params.field) { + classname = "hasError"; + } else if ( + params.field === "taskGroupId" && + params.row.isPlanned !== undefined && + !params.row.isPlanned + ) { + classname = "hasWarning"; + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} + /> + ); +}; + +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some leave entries!")} + + ); +}; + +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; + +export default EntryInputTable; diff --git a/src/components/LeaveTable/LeaveTable.tsx b/src/components/LeaveTable/LeaveTable.tsx new file mode 100644 index 0000000..5d0a003 --- /dev/null +++ b/src/components/LeaveTable/LeaveTable.tsx @@ -0,0 +1,133 @@ +import { RecordLeaveInput, LeaveEntry } from "@/app/api/timesheets/actions"; +import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; +import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material"; +import { + Box, + Collapse, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import dayjs from "dayjs"; +import React, { useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import LeaveEntryTable from "./LeaveEntryTable"; +import { LeaveType } from "@/app/api/timesheets"; + +interface Props { + leaveTypes: LeaveType[]; +} + +const MAX_HOURS = 8; + +const LeaveTable: React.FC = ({ leaveTypes }) => { + const { t } = useTranslation("home"); + + const { watch } = useFormContext(); + const currentInput = watch(); + const days = Object.keys(currentInput); + + return ( + + + + + + {t("Date")} + {t("Daily Total Hours")} + + + + {days.map((day, index) => { + const entries = currentInput[day]; + return ( + + ); + })} + +
+
+ ); +}; + +const DayRow: React.FC<{ + day: string; + entries: LeaveEntry[]; + leaveTypes: LeaveType[]; +}> = ({ day, entries, leaveTypes }) => { + const { + t, + i18n: { language }, + } = useTranslation("home"); + const dayJsObj = dayjs(day); + const [open, setOpen] = useState(false); + + const totalHours = entries.reduce((acc, entry) => acc + entry.inputHours, 0); + + return ( + <> + + + setOpen(!open)} + > + {open ? : } + + + + {shortDateFormatter(language).format(dayJsObj.toDate())} + + MAX_HOURS ? "error.main" : undefined }} + > + {manhourFormatter.format(totalHours)} + {totalHours > MAX_HOURS && ( + + {t("(the daily total hours cannot be more than 8.)")} + + )} + + + + + + + + + + + + + ); +}; + +export default LeaveTable; diff --git a/src/components/LeaveTable/index.ts b/src/components/LeaveTable/index.ts new file mode 100644 index 0000000..9aeb679 --- /dev/null +++ b/src/components/LeaveTable/index.ts @@ -0,0 +1 @@ +export { default } from "./LeaveTable"; diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index c336cef..e8e5061 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -24,7 +24,6 @@ import { AssignedProject } from "@/app/api/projects"; interface Props { isOpen: boolean; onClose: () => void; - timesheetType: "time" | "leave"; assignedProjects: AssignedProject[]; username: string; defaultTimesheets?: RecordTimesheetInput; @@ -43,7 +42,6 @@ const modalSx: SxProps = { const TimesheetModal: React.FC = ({ isOpen, onClose, - timesheetType, assignedProjects, username, defaultTimesheets, @@ -100,7 +98,7 @@ const TimesheetModal: React.FC = ({ onSubmit={formProps.handleSubmit(onSubmit)} > - {t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")} + {t("Timesheet Input")} = ({ + leaveTypes, assignedProjects, username, + defaultLeaveRecords, defaultTimesheets, }) => { const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); @@ -60,46 +69,36 @@ const UserWorkspacePage: React.FC = ({ flexWrap="wrap" spacing={2} > - {Boolean(assignedProjects.length) && ( - - - - - )} + + + + + + {assignedProjects.length > 0 ? ( - <> - - - - + ) : ( - <> - - {t("You have no assigned projects!")} - - + + {t("You have no assigned projects!")} + )} ); diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx index cd5fe66..529519e 100644 --- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx +++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx @@ -1,15 +1,21 @@ import { fetchAssignedProjects } from "@/app/api/projects"; import UserWorkspacePage from "./UserWorkspacePage"; -import { fetchTimesheets } from "@/app/api/timesheets"; +import { + fetchLeaveTypes, + fetchLeaves, + fetchTimesheets, +} from "@/app/api/timesheets"; interface Props { username: string; } const UserWorkspaceWrapper: React.FC = async ({ username }) => { - const [assignedProjects, timesheets] = await Promise.all([ - fetchAssignedProjects(), + const [assignedProjects, timesheets, leaves, leaveTypes] = await Promise.all([ + fetchAssignedProjects(username), fetchTimesheets(username), + fetchLeaves(username), + fetchLeaveTypes(), ]); return ( @@ -17,6 +23,8 @@ const UserWorkspaceWrapper: React.FC = async ({ username }) => { assignedProjects={assignedProjects} username={username} defaultTimesheets={timesheets} + defaultLeaveRecords={leaves} + leaveTypes={leaveTypes} /> ); }; From c217d880a3a352e67201f0f1a1b8716a198f1150 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 6 May 2024 18:17:24 +0800 Subject: [PATCH 2/2] update --- src/app/(main)/settings/group/create/page.tsx | 22 ++ src/app/(main)/settings/group/edit/page.tsx | 0 src/app/(main)/settings/group/page.tsx | 55 +++++ src/app/(main)/settings/team/create/page.tsx | 4 - src/app/(main)/settings/user/edit/page.tsx | 24 ++ src/app/(main)/settings/user/page.tsx | 4 +- src/app/api/group/actions.ts | 44 ++++ src/app/api/group/index.ts | 21 ++ src/app/api/user/actions.ts | 11 +- src/app/api/user/index.ts | 1 + .../CreateGroup/AuthorityAllocation.tsx | 211 ++++++++++++++++++ src/components/CreateGroup/CreateGroup.tsx | 130 +++++++++++ .../CreateGroup/CreateGroupLoading.tsx | 40 ++++ .../CreateGroup/CreateGroupWrapper.tsx | 24 ++ src/components/CreateGroup/GroupInfo.tsx | 81 +++++++ src/components/CreateGroup/UserAllocation.tsx | 209 +++++++++++++++++ src/components/CreateGroup/index.ts | 1 + src/components/CreateTeam/TeamInfo.tsx | 2 +- src/components/EditTeam/Allocation.tsx | 3 +- .../NavigationContent/NavigationContent.tsx | 1 + .../UserGroupSearch/UserGroupSearch.tsx | 94 ++++++++ .../UserGroupSearchLoading.tsx | 40 ++++ .../UserGroupSearchWrapper.tsx | 19 ++ src/components/UserGroupSearch/index.ts | 1 + 24 files changed, 1031 insertions(+), 11 deletions(-) create mode 100644 src/app/(main)/settings/group/create/page.tsx create mode 100644 src/app/(main)/settings/group/edit/page.tsx create mode 100644 src/app/(main)/settings/group/page.tsx create mode 100644 src/app/(main)/settings/user/edit/page.tsx create mode 100644 src/app/api/group/actions.ts create mode 100644 src/app/api/group/index.ts create mode 100644 src/components/CreateGroup/AuthorityAllocation.tsx create mode 100644 src/components/CreateGroup/CreateGroup.tsx create mode 100644 src/components/CreateGroup/CreateGroupLoading.tsx create mode 100644 src/components/CreateGroup/CreateGroupWrapper.tsx create mode 100644 src/components/CreateGroup/GroupInfo.tsx create mode 100644 src/components/CreateGroup/UserAllocation.tsx create mode 100644 src/components/CreateGroup/index.ts create mode 100644 src/components/UserGroupSearch/UserGroupSearch.tsx create mode 100644 src/components/UserGroupSearch/UserGroupSearchLoading.tsx create mode 100644 src/components/UserGroupSearch/UserGroupSearchWrapper.tsx create mode 100644 src/components/UserGroupSearch/index.ts diff --git a/src/app/(main)/settings/group/create/page.tsx b/src/app/(main)/settings/group/create/page.tsx new file mode 100644 index 0000000..9130236 --- /dev/null +++ b/src/app/(main)/settings/group/create/page.tsx @@ -0,0 +1,22 @@ +// 'use client'; +import { I18nProvider, getServerI18n } from "@/i18n"; +import React, { useCallback, useState } from "react"; +import { Typography } from "@mui/material"; +import CreateGroup from "@/components/CreateGroup"; + +// const Title = ["title1", "title2"]; + +const CreateStaff: React.FC = async () => { + const { t } = await getServerI18n("group"); + + return ( + <> + {t("Create Group")} + + + + + ); +}; + +export default CreateStaff; diff --git a/src/app/(main)/settings/group/edit/page.tsx b/src/app/(main)/settings/group/edit/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(main)/settings/group/page.tsx b/src/app/(main)/settings/group/page.tsx new file mode 100644 index 0000000..5322132 --- /dev/null +++ b/src/app/(main)/settings/group/page.tsx @@ -0,0 +1,55 @@ +import { preloadClaims } from "@/app/api/claims"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import StaffSearch from "@/components/StaffSearch"; +import TeamSearch from "@/components/TeamSearch"; +import UserGroupSearch from "@/components/UserGroupSearch"; +import UserSearch from "@/components/UserSearch"; +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: "User Group", + }; + + + const UserGroup: React.FC = async () => { + const { t } = await getServerI18n("User Group"); + // preloadTeamLeads(); + // preloadStaff(); + return ( + <> + + + {t("User Group")} + + + + + }> + + + + + ); + }; + + export default UserGroup; \ No newline at end of file diff --git a/src/app/(main)/settings/team/create/page.tsx b/src/app/(main)/settings/team/create/page.tsx index 721fda7..a47d81c 100644 --- a/src/app/(main)/settings/team/create/page.tsx +++ b/src/app/(main)/settings/team/create/page.tsx @@ -28,10 +28,6 @@ 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")} diff --git a/src/app/(main)/settings/user/edit/page.tsx b/src/app/(main)/settings/user/edit/page.tsx new file mode 100644 index 0000000..659347b --- /dev/null +++ b/src/app/(main)/settings/user/edit/page.tsx @@ -0,0 +1,24 @@ +import { Edit } from "@mui/icons-material"; +import { useSearchParams } from "next/navigation"; +// import EditStaff from "@/components/EditStaff"; +import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; +// import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; +import { Metadata } from "next"; +import EditUser from "@/components/EditUser"; + + +const EditUserPage: React.FC = () => { + + return ( + <> + + }> + + + + + ); +}; + +export default EditUserPage; diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx index 95973ab..ef7635f 100644 --- a/src/app/(main)/settings/user/page.tsx +++ b/src/app/(main)/settings/user/page.tsx @@ -33,14 +33,14 @@ export const metadata: Metadata = { {t("User")} - + */} }> diff --git a/src/app/api/group/actions.ts b/src/app/api/group/actions.ts new file mode 100644 index 0000000..c8881de --- /dev/null +++ b/src/app/api/group/actions.ts @@ -0,0 +1,44 @@ +"use server"; + +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; +import { cache } from "react"; + + +export interface CreateGroupInputs { + id?: number; + name: string; + description: string; + addUserIds?: number[]; + removeUserIds?: number[]; + addAuthIds?: number[]; + removeAuthIds?: number[]; + } + +export interface auth { + id: number; + module?: any | null; + authority: string; + name: string; + description: string | null; + v: number; + } + +export interface record { + records: auth[]; + } + + export const fetchAuth = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/group/auth/combo`, { + next: { tags: ["auth"] }, + }); + }); + +export const saveGroup = async (data: CreateGroupInputs) => { + return serverFetchJson(`${BASE_API_URL}/group/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/group/index.ts b/src/app/api/group/index.ts new file mode 100644 index 0000000..9dcee9e --- /dev/null +++ b/src/app/api/group/index.ts @@ -0,0 +1,21 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + +export interface Records { + records: UserGroupResult[] +} + +export interface UserGroupResult { + id: number; + action: () => void; + name: string; + description: string; +} + +export const fetchGroup = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/group`, { + next: { tags: ["group"] }, + }); + }); diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index 5df734a..4d353c3 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -8,8 +8,7 @@ import { cache } from "react"; export interface UserInputs { username: string; - firstname: string; - lastname: string; + email: string; } @@ -19,6 +18,14 @@ export const fetchUserDetails = cache(async (id: number) => { }); }); +export const editUser = async (id: number, data: UserInputs) => { + return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { + method: "PUT", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; + export const deleteUser = async (id: number) => { return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { method: "DELETE", diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts index 9a6065b..3151b64 100644 --- a/src/app/api/user/index.ts +++ b/src/app/api/user/index.ts @@ -19,6 +19,7 @@ export interface UserResult { phone1: string; phone2: string; remarks: string; + groupId: number; } // export interface DetailedUser extends UserResult { diff --git a/src/components/CreateGroup/AuthorityAllocation.tsx b/src/components/CreateGroup/AuthorityAllocation.tsx new file mode 100644 index 0000000..fd9610b --- /dev/null +++ b/src/components/CreateGroup/AuthorityAllocation.tsx @@ -0,0 +1,211 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import { + Box, + Card, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { differenceBy } from "lodash"; +import { CreateGroupInputs, auth } from "@/app/api/group/actions"; +import SearchResults, { Column } from "../SearchResults"; +import { Add, Clear, Remove, Search } from "@mui/icons-material"; + +export interface Props { + auth: auth[]; +} + +const AuthorityAllocation: React.FC = ({ auth }) => { + const { t } = useTranslation(); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + const initialAuths = auth.map((a) => ({ ...a })).sort((a, b) => a.id - b.id); + const [filteredAuths, setFilteredAuths] = useState(initialAuths); + const [selectedAuths, setSelectedAuths] = useState( + () => { + return filteredAuths.filter( + (s) => getValues("addAuthIds")?.includes(s.id) + ); + } + ); + // Adding / Removing Auth + const addAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => [...a, auth]); + }, []); + const removeAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => a.filter((a) => a.id !== auth.id)); + }, []); + + const clearAuth = useCallback(() => { + if (defaultValues !== undefined) { + resetField("addAuthIds"); + setSelectedAuths( + initialAuths.filter((s) => defaultValues.addAuthIds?.includes(s.id)) + ); + } + }, [defaultValues]); + + // Sync with form + useEffect(() => { + setValue( + "addAuthIds", + selectedAuths.map((a) => a.id) + ); + }, [selectedAuths, setValue]); + + const AuthPoolColumns = useMemo[]>( + () => [ + { + label: t("Add"), + name: "id", + onClick: addAuth, + buttonIcon: , + }, + { label: t("authority"), name: "authority" }, + { label: t("Auth Name"), name: "name" }, + // { label: t("Current Position"), name: "currentPosition" }, + ], + [addAuth, t] + ); + + const allocatedAuthColumns = useMemo[]>( + () => [ + { + label: t("Remove"), + name: "id", + onClick: removeAuth, + buttonIcon: , + }, + { label: t("authority"), name: "authority" }, + { label: t("Auth Name"), name: "name" }, + ], + [removeAuth, selectedAuths, 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)) + // }) + // ); + }, [auth, query]); + + useEffect(() => { + // console.log(getValues("addStaffIds")) + }, [initialAuths]); + + const resetAuth = React.useCallback(() => { + clearQueryInput(); + clearAuth(); + }, [clearQueryInput, clearAuth]); + + const formProps = useForm({}); + + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + return ( + <> + + + + + + {t("Authority")} + + + + + + + + + + ), + }} + /> + + + + + + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + + + + + + + ); +}; + +export default AuthorityAllocation; diff --git a/src/components/CreateGroup/CreateGroup.tsx b/src/components/CreateGroup/CreateGroup.tsx new file mode 100644 index 0000000..e931521 --- /dev/null +++ b/src/components/CreateGroup/CreateGroup.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { CreateGroupInputs, auth, saveGroup } from "@/app/api/group/actions"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { FieldErrors, FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; +import { Check, Close, Error } from "@mui/icons-material"; +import GroupInfo from "./GroupInfo"; +import AuthorityAllocation from "./AuthorityAllocation"; +import UserAllocation from "./UserAllocation"; +import { UserResult } from "@/app/api/user"; + +interface Props { + auth?: auth[] + users?: UserResult[] +} + +const CreateGroup: React.FC = ({ auth, users }) => { + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const [tabIndex, setTabIndex] = useState(0); + const { t } = useTranslation(); + + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + const postData = { + ...data, + removeUserIds: [], + removeAuthIds: [], + + } + console.log(postData) + await saveGroup(postData) + router.replace("/settings/group") + } 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 && } + {tabIndex === 2 && } + + + + + + + + + ); +}; + +export default CreateGroup; diff --git a/src/components/CreateGroup/CreateGroupLoading.tsx b/src/components/CreateGroup/CreateGroupLoading.tsx new file mode 100644 index 0000000..6a48c4e --- /dev/null +++ b/src/components/CreateGroup/CreateGroupLoading.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 CreateGroupLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + Create Group + + + + + + + + + + + ); +}; + +export default CreateGroupLoading; diff --git a/src/components/CreateGroup/CreateGroupWrapper.tsx b/src/components/CreateGroup/CreateGroupWrapper.tsx new file mode 100644 index 0000000..e4bd018 --- /dev/null +++ b/src/components/CreateGroup/CreateGroupWrapper.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import CreateGroupLoading from "./CreateGroupLoading"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import { useSearchParams } from "next/navigation"; +import CreateGroup from "./CreateGroup"; +import { auth, fetchAuth } from "@/app/api/group/actions"; +import { fetchUser } from "@/app/api/user"; + +interface SubComponents { + Loading: typeof CreateGroupLoading; +} + +const CreateGroupWrapper: React.FC & SubComponents = async () => { + const records = await fetchAuth() + const users = await fetchUser() + console.log(users) + const auth = records.records as auth[] + + return ; +}; + +CreateGroupWrapper.Loading = CreateGroupLoading; + +export default CreateGroupWrapper; diff --git a/src/components/CreateGroup/GroupInfo.tsx b/src/components/CreateGroup/GroupInfo.tsx new file mode 100644 index 0000000..d9141bc --- /dev/null +++ b/src/components/CreateGroup/GroupInfo.tsx @@ -0,0 +1,81 @@ +"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 { CreateGroupInputs } from "@/app/api/group/actions"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useCallback } from "react"; + +const GroupInfo: React.FC = () => { + const { t } = useTranslation(); + const { + register, + formState: { errors, defaultValues }, + control, + reset, + resetField, + setValue, + } = useFormContext(); + + + const resetGroup = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("description"); + } + }, [defaultValues]); + + + return ( + + + + + {t("Group Info")} + + + + + + + + + + + + + ); +}; + +export default GroupInfo; diff --git a/src/components/CreateGroup/UserAllocation.tsx b/src/components/CreateGroup/UserAllocation.tsx new file mode 100644 index 0000000..ff13c52 --- /dev/null +++ b/src/components/CreateGroup/UserAllocation.tsx @@ -0,0 +1,209 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import { + Box, + Card, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { differenceBy } from "lodash"; +import { CreateGroupInputs, auth } from "@/app/api/group/actions"; +import SearchResults, { Column } from "../SearchResults"; +import { Add, Clear, Remove, Search } from "@mui/icons-material"; +import { UserResult } from "@/app/api/user"; + +export interface Props { + users: UserResult[]; +} + +const UserAllocation: React.FC = ({ users }) => { + const { t } = useTranslation(); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + const initialUsers = users.map((u) => ({ ...u })).sort((a, b) => a.id - b.id).filter((u) => u.groupId !== null); + const [filteredUsers, setFilteredUsers] = useState(initialUsers); + const [selectedUsers, setSelectedUsers] = useState( + () => { + return filteredUsers.filter( + (s) => getValues("addUserIds")?.includes(s.id) + ); + } + ); + // Adding / Removing Auth + const addUser = useCallback((users: UserResult) => { + setSelectedUsers((a) => [...a, users]); + }, []); + + const removeUser = useCallback((users: UserResult) => { + setSelectedUsers((a) => a.filter((a) => a.id !== users.id)); + }, []); + + const clearUser = useCallback(() => { + if (defaultValues !== undefined) { + resetField("addUserIds"); + setSelectedUsers( + initialUsers.filter((s) => defaultValues.addUserIds?.includes(s.id)) + ); + } + }, [defaultValues]); + + // Sync with form + useEffect(() => { + setValue( + "addUserIds", + selectedUsers.map((u) => u.id) + ); + }, [selectedUsers, setValue]); + + const UserPoolColumns = useMemo[]>( + () => [ + { + label: t("Add"), + name: "id", + onClick: addUser, + buttonIcon: , + }, + { label: t("User Name"), name: "username" }, + { label: t("name"), name: "name" }, + ], + [addUser, t] + ); + + const allocatedUserColumns = useMemo[]>( + () => [ + { + label: t("Remove"), + name: "id", + onClick: removeUser, + buttonIcon: , + }, + { label: t("User Name"), name: "username" }, + { label: t("name"), name: "name" }, + ], + [removeUser, selectedUsers, 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)) + // }) + // ); + }, [users, query]); + + const resetUser = React.useCallback(() => { + clearQueryInput(); + clearUser(); + }, [clearQueryInput, clearUser]); + + const formProps = useForm({}); + + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + return ( + <> + + + + + + {t("User")} + + + + + + + + + + ), + }} + /> + + + + + + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + + + + + + + ); +}; + +export default UserAllocation; diff --git a/src/components/CreateGroup/index.ts b/src/components/CreateGroup/index.ts new file mode 100644 index 0000000..1034fc8 --- /dev/null +++ b/src/components/CreateGroup/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateGroupWrapper" \ No newline at end of file diff --git a/src/components/CreateTeam/TeamInfo.tsx b/src/components/CreateTeam/TeamInfo.tsx index 4e61f4b..cd8b90a 100644 --- a/src/components/CreateTeam/TeamInfo.tsx +++ b/src/components/CreateTeam/TeamInfo.tsx @@ -27,7 +27,7 @@ const TeamInfo: React.FC = ( setValue, } = useFormContext(); - const resetCustomer = useCallback(() => { + const resetTeam = useCallback(() => { console.log(defaultValues); if (defaultValues !== undefined) { resetField("description"); diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 2599867..61e9e8f 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -49,7 +49,7 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { reset, resetField, } = useFormContext(); - + // let firstFilter: StaffResult[] = [] const initialStaffs = staff.map((s) => ({ ...s })); @@ -63,7 +63,6 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) } ); - console.log(filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id))) const [seletedTeamLead, setSeletedTeamLead] = useState(); const [deletedStaffIds, setDeletedStaffIds] = useState([]); diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 233f228..226482e 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -143,6 +143,7 @@ const NavigationContent: React.FC = ({ abilities }) => { { icon: , label: "Salary", path: "/settings/salary" }, { icon: , label: "Team", path: "/settings/team" }, { icon: , label: "User", path: "/settings/user" }, + { icon: , label: "User Group", path: "/settings/group" }, { icon: , label: "Holiday", path: "/settings/holiday" }, ], }, diff --git a/src/components/UserGroupSearch/UserGroupSearch.tsx b/src/components/UserGroupSearch/UserGroupSearch.tsx new file mode 100644 index 0000000..0480167 --- /dev/null +++ b/src/components/UserGroupSearch/UserGroupSearch.tsx @@ -0,0 +1,94 @@ +"use client"; + +import SearchBox, { Criterion } from "../SearchBox"; +import { useCallback, 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 { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { UserGroupResult } from "@/app/api/group"; +import { deleteUser } from "@/app/api/user/actions"; + +interface Props { + users: UserGroupResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const UserGroupSearch: React.FC = ({ users }) => { + const { t } = useTranslation(); + const [filteredUser, setFilteredUser] = useState(users); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("User Name"), + paramName: "name", + type: "text", + }, + ], + [t] + ); + + const onUserClick = useCallback( + (users: UserGroupResult) => { + console.log(users); + // router.push(`/settings/user/edit?id=${users.id}`) + }, + [router, t] + ); + + const onDeleteClick = useCallback((users: UserGroupResult) => { + deleteDialog(async () => { + await deleteUser(users.id); + + successDialog(t("Delete Success"), t); + + setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); + }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onUserClick, + buttonIcon: , + }, + { name: "name", label: t("Group Name") }, + { name: "description", label: t("Description") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); + + return ( + <> + { + // setFilteredUser( + // users.filter( + // (t) => + // t.name.toLowerCase().includes(query.name.toLowerCase()) && + // t.code.toLowerCase().includes(query.code.toLowerCase()) && + // t.description.toLowerCase().includes(query.description.toLowerCase()) + // ) + // ) + }} + /> + items={filteredUser} columns={columns} /> + + ); +}; +export default UserGroupSearch; diff --git a/src/components/UserGroupSearch/UserGroupSearchLoading.tsx b/src/components/UserGroupSearch/UserGroupSearchLoading.tsx new file mode 100644 index 0000000..5d8df0f --- /dev/null +++ b/src/components/UserGroupSearch/UserGroupSearchLoading.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 UserGroupSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default UserGroupSearchLoading; diff --git a/src/components/UserGroupSearch/UserGroupSearchWrapper.tsx b/src/components/UserGroupSearch/UserGroupSearchWrapper.tsx new file mode 100644 index 0000000..9f792ed --- /dev/null +++ b/src/components/UserGroupSearch/UserGroupSearchWrapper.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import UserGroupSearchLoading from "./UserGroupSearchLoading"; +import { UserGroupResult, fetchGroup } from "@/app/api/group"; +import UserGroupSearch from "./UserGroupSearch"; + +interface SubComponents { + Loading: typeof UserGroupSearchLoading; +} + +const UserGroupSearchWrapper: React.FC & SubComponents = async () => { +const group = await fetchGroup() + console.log(group.records); + + return ; +}; + +UserGroupSearchWrapper.Loading = UserGroupSearchLoading; + +export default UserGroupSearchWrapper; diff --git a/src/components/UserGroupSearch/index.ts b/src/components/UserGroupSearch/index.ts new file mode 100644 index 0000000..f2e5e63 --- /dev/null +++ b/src/components/UserGroupSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./UserGroupSearchWrapper";