Переглянути джерело

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 рік тому
джерело
коміт
65be878973
37 змінених файлів з 1684 додано та 61 видалено
  1. +10
    -2
      src/app/(main)/home/page.tsx
  2. +22
    -0
      src/app/(main)/settings/group/create/page.tsx
  3. +0
    -0
      src/app/(main)/settings/group/edit/page.tsx
  4. +55
    -0
      src/app/(main)/settings/group/page.tsx
  5. +0
    -4
      src/app/(main)/settings/team/create/page.tsx
  6. +24
    -0
      src/app/(main)/settings/user/edit/page.tsx
  7. +2
    -2
      src/app/(main)/settings/user/page.tsx
  8. +44
    -0
      src/app/api/group/actions.ts
  9. +21
    -0
      src/app/api/group/index.ts
  10. +2
    -2
      src/app/api/projects/actions.ts
  11. +2
    -2
      src/app/api/projects/index.ts
  12. +25
    -0
      src/app/api/timesheets/actions.ts
  13. +21
    -1
      src/app/api/timesheets/index.ts
  14. +9
    -2
      src/app/api/user/actions.ts
  15. +1
    -0
      src/app/api/user/index.ts
  16. +211
    -0
      src/components/CreateGroup/AuthorityAllocation.tsx
  17. +130
    -0
      src/components/CreateGroup/CreateGroup.tsx
  18. +40
    -0
      src/components/CreateGroup/CreateGroupLoading.tsx
  19. +24
    -0
      src/components/CreateGroup/CreateGroupWrapper.tsx
  20. +81
    -0
      src/components/CreateGroup/GroupInfo.tsx
  21. +209
    -0
      src/components/CreateGroup/UserAllocation.tsx
  22. +1
    -0
      src/components/CreateGroup/index.ts
  23. +1
    -1
      src/components/CreateTeam/TeamInfo.tsx
  24. +1
    -2
      src/components/EditTeam/Allocation.tsx
  25. +127
    -0
      src/components/LeaveModal/LeaveModal.tsx
  26. +1
    -0
      src/components/LeaveModal/index.ts
  27. +283
    -0
      src/components/LeaveTable/LeaveEntryTable.tsx
  28. +133
    -0
      src/components/LeaveTable/LeaveTable.tsx
  29. +1
    -0
      src/components/LeaveTable/index.ts
  30. +1
    -0
      src/components/NavigationContent/NavigationContent.tsx
  31. +1
    -3
      src/components/TimesheetModal/TimesheetModal.tsx
  32. +94
    -0
      src/components/UserGroupSearch/UserGroupSearch.tsx
  33. +40
    -0
      src/components/UserGroupSearch/UserGroupSearchLoading.tsx
  34. +19
    -0
      src/components/UserGroupSearch/UserGroupSearchWrapper.tsx
  35. +1
    -0
      src/components/UserGroupSearch/index.ts
  36. +36
    -37
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  37. +11
    -3
      src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx

+ 10
- 2
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 (
<I18nProvider namespaces={["home"]}>


+ 22
- 0
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 (
<>
<Typography variant="h4">{t("Create Group")}</Typography>
<I18nProvider namespaces={["group"]}>
<CreateGroup />
</I18nProvider>
</>
);
};

export default CreateStaff;

+ 0
- 0
src/app/(main)/settings/group/edit/page.tsx Переглянути файл


+ 55
- 0
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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("User Group")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/group/create"
>
{t("Create User Group")}
</Button>
</Stack>
<I18nProvider namespaces={["User Group", "common"]}>
<Suspense fallback={<UserGroupSearch.Loading />}>
<UserGroupSearch />
</Suspense>
</I18nProvider>
</>
);
};
export default UserGroup;

+ 0
- 4
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 (
<>
<Typography variant="h4">{t("Create Team")}</Typography>


+ 24
- 0
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 (
<>
<I18nProvider namespaces={["team", "common"]}>
<Suspense fallback={<EditUser.Loading />}>
<EditUser />
</Suspense>
</I18nProvider>
</>
);
};

export default EditUserPage;

+ 2
- 2
src/app/(main)/settings/user/page.tsx Переглянути файл

@@ -33,14 +33,14 @@ export const metadata: Metadata = {
<Typography variant="h4" marginInlineEnd={2}>
{t("User")}
</Typography>
<Button
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/team/create"
>
{t("Create User")}
</Button>
</Button> */}
</Stack>
<I18nProvider namespaces={["User", "common"]}>
<Suspense fallback={<UserSearch.Loading />}>


+ 44
- 0
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<record>(`${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" },
});
};

+ 21
- 0
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<Records>(`${BASE_API_URL}/group`, {
next: { tags: ["group"] },
});
});

+ 2
- 2
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;
};

+ 2
- 2
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<AssignedProject[]>(
`${BASE_API_URL}/projects/assignedProjects`,
{
next: { tags: ["assignedProjects"] },
next: { tags: [`assignedProjects__${username}`] },
},
);
});


+ 25
- 0
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<RecordLeaveInput>(
`${BASE_API_URL}/timesheets/saveLeave`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

revalidateTag(`leaves_${username}`);

return savedRecords;
};

+ 21
- 1
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<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, {
next: { tags: [`timesheets_${username}`] },
});
});

export const fetchLeaves = cache(async (username: string) => {
return serverFetchJson<RecordLeaveInput>(
`${BASE_API_URL}/timesheets/leaves`,
{
next: { tags: [`leaves_${username}`] },
},
);
});

export const fetchLeaveTypes = cache(async () => {
return serverFetchJson<LeaveType[]>(`${BASE_API_URL}/timesheets/leaveTypes`, {
next: { tags: ["leaveTypes"] },
});
});

+ 9
- 2
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",


+ 1
- 0
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 {


+ 211
- 0
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<Props> = ({ auth }) => {
const { t } = useTranslation();
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<CreateGroupInputs>();
const initialAuths = auth.map((a) => ({ ...a })).sort((a, b) => a.id - b.id);
const [filteredAuths, setFilteredAuths] = useState(initialAuths);
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>(
() => {
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<Column<auth>[]>(
() => [
{
label: t("Add"),
name: "id",
onClick: addAuth,
buttonIcon: <Add />,
},
{ label: t("authority"), name: "authority" },
{ label: t("Auth Name"), name: "name" },
// { label: t("Current Position"), name: "currentPosition" },
],
[addAuth, t]
);

const allocatedAuthColumns = useMemo<Column<auth>[]>(
() => [
{
label: t("Remove"),
name: "id",
onClick: removeAuth,
buttonIcon: <Remove color="warning"/>,
},
{ 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<HTMLInputElement>
>((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<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

return (
<>
<FormProvider {...formProps}>
<Card sx={{ display: "block" }}>
<CardContent
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("Authority")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t("Search by staff ID, name or position.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Authority Pool")} />
<Tab
label={`${t("Allocated Authority")} (${selectedAuths.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredAuths, selectedAuths, "id")}
columns={AuthPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedAuths}
columns={allocatedAuthColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>
</FormProvider>
</>
);
};

export default AuthorityAllocation;

+ 130
- 0
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<Props> = ({ auth, users }) => {
const formProps = useForm<CreateGroupInputs>();
const [serverError, setServerError] = useState("");
const router = useRouter();
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();

const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateGroupInputs>>(
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<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateGroupInputs>,
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};

return (
<>
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("Group Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("Authority Allocation")} iconPosition="end" />
<Tab label={t("User Allocation")} iconPosition="end" />
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <GroupInfo/>}
{tabIndex === 1 && <AuthorityAllocation auth={auth!!}/>}
{tabIndex === 2 && <UserAllocation users={users!!}/>}

<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button
variant="contained"
startIcon={<Check />}
type="submit"
// disabled={Boolean(formProps.watch("isGridEditing"))}
>
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};

export default CreateGroup;

+ 40
- 0
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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>Create Group
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default CreateGroupLoading;

+ 24
- 0
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 <CreateGroup auth={auth} users={users}/>;
};

CreateGroupWrapper.Loading = CreateGroupLoading;

export default CreateGroupWrapper;

+ 81
- 0
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<CreateGroupInputs>();

const resetGroup = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("description");
}
}, [defaultValues]);


return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Group Info")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Group Name")}
fullWidth
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={
Boolean(errors.name) &&
(errors.name?.message
? t(errors.name.message)
: t("Please input correct name"))
}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Group Description")}
fullWidth
multiline
rows={4}
{...register("description")}
error={Boolean(errors.description)}
helperText={
Boolean(errors.description) &&
(errors.description?.message
? t(errors.description.message)
: t("Please input correct description"))
}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
);
};

export default GroupInfo;

+ 209
- 0
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<Props> = ({ users }) => {
const { t } = useTranslation();
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<CreateGroupInputs>();
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<typeof filteredUsers>(
() => {
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<Column<UserResult>[]>(
() => [
{
label: t("Add"),
name: "id",
onClick: addUser,
buttonIcon: <Add />,
},
{ label: t("User Name"), name: "username" },
{ label: t("name"), name: "name" },
],
[addUser, t]
);

const allocatedUserColumns = useMemo<Column<UserResult>[]>(
() => [
{
label: t("Remove"),
name: "id",
onClick: removeUser,
buttonIcon: <Remove color="warning" />,
},
{ 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<HTMLInputElement>
>((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<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

return (
<>
<FormProvider {...formProps}>
<Card sx={{ display: "block" }}>
<CardContent
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("User")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t("Search by staff ID, name or position.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("User Pool")} />
<Tab
label={`${t("Allocated Users")} (${selectedUsers.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredUsers, selectedUsers, "id")}
columns={UserPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedUsers}
columns={allocatedUserColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>
</FormProvider>
</>
);
};

export default UserAllocation;

+ 1
- 0
src/components/CreateGroup/index.ts Переглянути файл

@@ -0,0 +1 @@
export { default } from "./CreateGroupWrapper"

+ 1
- 1
src/components/CreateTeam/TeamInfo.tsx Переглянути файл

@@ -27,7 +27,7 @@ const TeamInfo: React.FC = (
setValue,
} = useFormContext<CreateTeamInputs>();

const resetCustomer = useCallback(() => {
const resetTeam = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("description");


+ 1
- 2
src/components/EditTeam/Allocation.tsx Переглянути файл

@@ -49,7 +49,7 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
reset,
resetField,
} = useFormContext<CreateTeamInputs>();
// let firstFilter: StaffResult[] = []

const initialStaffs = staff.map((s) => ({ ...s }));
@@ -63,7 +63,6 @@ const Allocation: React.FC<Props> = ({ 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<number>();
const [deletedStaffIds, setDeletedStaffIds] = useState<number[]>([]);



+ 127
- 0
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<Props> = ({
isOpen,
onClose,
username,
defaultLeaveRecords,
leaveTypes,
}) => {
const { t } = useTranslation("home");

const defaultValues = useMemo(() => {
const today = dayjs();
return Array(7)
.fill(undefined)
.reduce<RecordLeaveInput>((acc, _, index) => {
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
return {
...acc,
[date]: defaultLeaveRecords?.[date] ?? [],
};
}, {});
}, [defaultLeaveRecords]);

const formProps = useForm<RecordLeaveInput>({ defaultValues });

const onSubmit = useCallback<SubmitHandler<RecordLeaveInput>>(
async (data) => {
const savedRecords = await saveLeave(data, username);

const today = dayjs();
const newFormValues = Array(7)
.fill(undefined)
.reduce<RecordLeaveInput>((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 (
<Modal open={isOpen} onClose={onClose}>
<Card sx={modalSx}>
<FormProvider {...formProps}>
<CardContent
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Record Leave")}
</Typography>
<Box
sx={{
marginInline: -3,
marginBlock: 4,
}}
>
<LeaveTable leaveTypes={leaveTypes} />
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={onCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Save")}
</Button>
</CardActions>
</CardContent>
</FormProvider>
</Card>
</Modal>
);
};

export default LeaveModal;

+ 1
- 0
src/components/LeaveModal/index.ts Переглянути файл

@@ -0,0 +1 @@
export { default } from "./LeaveModal";

+ 283
- 0
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<Props> = ({ day, leaveTypes }) => {
const { t } = useTranslation("home");

const { getValues, setValue } = useFormContext<RecordLeaveInput>();
const currentEntries = getValues(day);

const [entries, setEntries] = useState<LeaveEntryRow[]>(currentEntries || []);

const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});

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<GridEventListener<"rowEditStop">>(
(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<GridColDef[]>(
() => [
{
type: "actions",
field: "actions",
headerName: t("Actions"),
getActions: ({ id }) => {
if (rowModesModel[id]?.mode === GridRowModes.Edit) {
return [
<GridActionsCellItem
key="accpet-action"
icon={<Check />}
label={t("Save")}
onClick={handleSave(id)}
/>,
<GridActionsCellItem
key="cancel-action"
icon={<Close />}
label={t("Cancel")}
onClick={handleCancel(id)}
/>,
];
}

return [
<GridActionsCellItem
key="delete-action"
icon={<Delete />}
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 = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Record leave")}
</Button>
</Box>
);

return (
<StyledDataGrid
apiRef={apiRef}
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
editMode="row"
rows={entries}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleEditStop}
processRowUpdate={processRowUpdate}
columns={columns}
getCellClassName={(params) => {
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 (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some leave entries!")}</Typography>
</Box>
);
};

const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};

export default EntryInputTable;

+ 133
- 0
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<Props> = ({ leaveTypes }) => {
const { t } = useTranslation("home");

const { watch } = useFormContext<RecordLeaveInput>();
const currentInput = watch();
const days = Object.keys(currentInput);

return (
<TableContainer sx={{ maxHeight: 400 }}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell />
<TableCell>{t("Date")}</TableCell>
<TableCell>{t("Daily Total Hours")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{days.map((day, index) => {
const entries = currentInput[day];
return (
<DayRow
key={`${day}${index}`}
day={day}
entries={entries}
leaveTypes={leaveTypes}
/>
);
})}
</TableBody>
</Table>
</TableContainer>
);
};

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 (
<>
<TableRow>
<TableCell align="center" width={70}>
<IconButton
disableRipple
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
</IconButton>
</TableCell>
<TableCell
sx={{ color: dayJsObj.day() === 0 ? "error.main" : undefined }}
>
{shortDateFormatter(language).format(dayJsObj.toDate())}
</TableCell>
<TableCell
sx={{ color: totalHours > MAX_HOURS ? "error.main" : undefined }}
>
{manhourFormatter.format(totalHours)}
{totalHours > MAX_HOURS && (
<Typography
color="error.main"
variant="body2"
component="span"
sx={{ marginInlineStart: 1 }}
>
{t("(the daily total hours cannot be more than 8.)")}
</Typography>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell
sx={{
p: 0,
border: "none",
outline: open ? "1px solid" : undefined,
outlineColor: "primary.main",
}}
colSpan={3}
>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<LeaveEntryTable day={day} leaveTypes={leaveTypes} />
</Box>
</Collapse>
</TableCell>
</TableRow>
</>
);
};

export default LeaveTable;

+ 1
- 0
src/components/LeaveTable/index.ts Переглянути файл

@@ -0,0 +1 @@
export { default } from "./LeaveTable";

+ 1
- 0
src/components/NavigationContent/NavigationContent.tsx Переглянути файл

@@ -143,6 +143,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => {
{ icon: <Salary />, label: "Salary", path: "/settings/salary" },
{ icon: <Team />, label: "Team", path: "/settings/team" },
{ icon: <ManageAccountsIcon />, label: "User", path: "/settings/user" },
{ icon: <ManageAccountsIcon />, label: "User Group", path: "/settings/group" },
{ icon: <Holiday />, label: "Holiday", path: "/settings/holiday" },
],
},


+ 1
- 3
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<Props> = ({
isOpen,
onClose,
timesheetType,
assignedProjects,
username,
defaultTimesheets,
@@ -100,7 +98,7 @@ const TimesheetModal: React.FC<Props> = ({
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")}
{t("Timesheet Input")}
</Typography>
<Box
sx={{


+ 94
- 0
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<Omit<UserGroupResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const UserGroupSearch: React.FC<Props> = ({ users }) => {
const { t } = useTranslation();
const [filteredUser, setFilteredUser] = useState(users);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = 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<Column<UserGroupResult>[]>(
() => [
{
name: "action",
label: t("Edit"),
onClick: onUserClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("Group Name") },
{ name: "description", label: t("Description") },
{
name: "action",
label: t("Delete"),
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
color: "error"
},
],
[t]
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// 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())
// )
// )
}}
/>
<SearchResults<UserGroupResult> items={filteredUser} columns={columns} />
</>
);
};
export default UserGroupSearch;

+ 40
- 0
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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default UserGroupSearchLoading;

+ 19
- 0
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 <UserGroupSearch users={group.records} />;
};

UserGroupSearchWrapper.Loading = UserGroupSearchLoading;

export default UserGroupSearchWrapper;

+ 1
- 0
src/components/UserGroupSearch/index.ts Переглянути файл

@@ -0,0 +1 @@
export { default } from "./UserGroupSearchWrapper";

+ 36
- 37
src/components/UserWorkspacePage/UserWorkspacePage.tsx Переглянути файл

@@ -10,17 +10,26 @@ import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects";
import TimesheetModal from "../TimesheetModal";
import { AssignedProject } from "@/app/api/projects";
import { RecordTimesheetInput } from "@/app/api/timesheets/actions";
import {
RecordLeaveInput,
RecordTimesheetInput,
} from "@/app/api/timesheets/actions";
import LeaveModal from "../LeaveModal";
import { LeaveType } from "@/app/api/timesheets";

export interface Props {
leaveTypes: LeaveType[];
assignedProjects: AssignedProject[];
username: string;
defaultLeaveRecords: RecordLeaveInput;
defaultTimesheets: RecordTimesheetInput;
}

const UserWorkspacePage: React.FC<Props> = ({
leaveTypes,
assignedProjects,
username,
defaultLeaveRecords,
defaultTimesheets,
}) => {
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false);
@@ -60,46 +69,36 @@ const UserWorkspacePage: React.FC<Props> = ({
flexWrap="wrap"
spacing={2}
>
{Boolean(assignedProjects.length) && (
<ButtonGroup variant="contained">
<Button
startIcon={<Add />}
onClick={handleAddTimesheetButtonClick}
>
{t("Enter Time")}
</Button>
<Button startIcon={<Add />} onClick={handleAddLeaveButtonClick}>
{t("Record Leave")}
</Button>
</ButtonGroup>
)}
<ButtonGroup variant="contained">
<Button startIcon={<Add />} onClick={handleAddTimesheetButtonClick}>
{t("Enter Time")}
</Button>
<Button startIcon={<Add />} onClick={handleAddLeaveButtonClick}>
{t("Record Leave")}
</Button>
</ButtonGroup>
</Stack>
</Stack>
<TimesheetModal
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
assignedProjects={assignedProjects}
username={username}
defaultTimesheets={defaultTimesheets}
/>
<LeaveModal
leaveTypes={leaveTypes}
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
defaultLeaveRecords={defaultLeaveRecords}
username={username}
/>
{assignedProjects.length > 0 ? (
<>
<TimesheetModal
timesheetType="time"
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
assignedProjects={assignedProjects}
username={username}
defaultTimesheets={defaultTimesheets}
/>
<TimesheetModal
timesheetType="leave"
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
assignedProjects={assignedProjects}
username={username}
/>
<AssignedProjects assignedProjects={assignedProjects} />
</>
<AssignedProjects assignedProjects={assignedProjects} />
) : (
<>
<Typography variant="subtitle1">
{t("You have no assigned projects!")}
</Typography>
</>
<Typography variant="subtitle1">
{t("You have no assigned projects!")}
</Typography>
)}
</>
);


+ 11
- 3
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<Props> = 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<Props> = async ({ username }) => {
assignedProjects={assignedProjects}
username={username}
defaultTimesheets={timesheets}
defaultLeaveRecords={leaves}
leaveTypes={leaveTypes}
/>
);
};


Завантаження…
Відмінити
Зберегти