2. Create Department Page 3. Postion Search Page 4. Create Position Pagetags/Baseline_30082024_FRONTEND_UAT
@@ -0,0 +1,25 @@ | |||
import CreateDepartment from "@/components/CreateDepartment"; | |||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
export const metadata: Metadata = { | |||
title: "Create Department", | |||
}; | |||
const Department: React.FC = async () => { | |||
const { t } = await getServerI18n("departments"); | |||
// Preload necessary dependencies | |||
return ( | |||
<> | |||
<Typography variant="h4">{t("Create Department")}</Typography> | |||
<I18nProvider namespaces={["departments"]}> | |||
<CreateDepartment /> | |||
</I18nProvider> | |||
</> | |||
); | |||
}; | |||
export default Department; |
@@ -0,0 +1,50 @@ | |||
import DepartmentSearch from "@/components/DepartmentSearch"; | |||
import { Metadata } from "next"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Typography from "@mui/material/Typography"; | |||
import Link from "next/link"; | |||
import { Suspense } from "react"; | |||
import { fetchDepartments, preloadDepartments } from "@/app/api/departments"; | |||
export const metadata: Metadata = { | |||
title: "Department", | |||
}; | |||
const Department: React.FC = async () => { | |||
const { t } = await getServerI18n("department"); | |||
// Preload necessary dependencies | |||
// fetchDepartments(); | |||
// preloadDepartments(); | |||
return ( | |||
<> | |||
<Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
rowGap={2} | |||
> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("Department")} | |||
</Typography> | |||
<Button | |||
variant="contained" | |||
startIcon={<Add />} | |||
LinkComponent={Link} | |||
href="/settings/department/new" | |||
> | |||
{t("Create Department")} | |||
</Button> | |||
</Stack> | |||
<Suspense fallback={<DepartmentSearch.Loading />}> | |||
<DepartmentSearch/> | |||
</Suspense> | |||
</> | |||
) | |||
}; | |||
export default Department; |
@@ -0,0 +1,25 @@ | |||
import CreatePosition from "@/components/CreatePosition"; | |||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
export const metadata: Metadata = { | |||
title: "Create Position", | |||
}; | |||
const Positions: React.FC = async () => { | |||
const { t } = await getServerI18n("positions"); | |||
// Preload necessary dependencies | |||
return ( | |||
<> | |||
<Typography variant="h4">{t("Create Position")}</Typography> | |||
<I18nProvider namespaces={["positions"]}> | |||
<CreatePosition /> | |||
</I18nProvider> | |||
</> | |||
); | |||
}; | |||
export default Positions; |
@@ -0,0 +1,50 @@ | |||
import PositionSearch from "@/components/PositionSearch"; | |||
import { Metadata } from "next"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Typography from "@mui/material/Typography"; | |||
import Link from "next/link"; | |||
import { Suspense } from "react"; | |||
import { fetchPositions, preloadPositions } from "@/app/api/positions"; | |||
export const metadata: Metadata = { | |||
title: "Position", | |||
}; | |||
const Position: React.FC = async () => { | |||
const { t } = await getServerI18n("Position"); | |||
// Preload necessary dependencies | |||
// fetchPositions(); | |||
// preloadPositions(); | |||
return ( | |||
<> | |||
<Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
rowGap={2} | |||
> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("Position")} | |||
</Typography> | |||
<Button | |||
variant="contained" | |||
startIcon={<Add />} | |||
LinkComponent={Link} | |||
href="/settings/position/new" | |||
> | |||
{t("Create Position")} | |||
</Button> | |||
</Stack> | |||
<Suspense fallback={<PositionSearch.Loading />}> | |||
<PositionSearch/> | |||
</Suspense> | |||
</> | |||
) | |||
}; | |||
export default Position; |
@@ -0,0 +1,18 @@ | |||
"use server" | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
export interface CreateDepartmentInputs { | |||
departmentCode: string; | |||
departmentName: string; | |||
description: string; | |||
} | |||
export const saveDepartment = async (data: CreateDepartmentInputs) => { | |||
return serverFetchJson(`${BASE_API_URL}/departments/new`, { | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; |
@@ -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 DepartmentResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
description: string; | |||
} | |||
export const preloadDepartments = () => { | |||
fetchDepartments(); | |||
}; | |||
export const fetchDepartments = cache(async () => { | |||
return serverFetchJson<DepartmentResult[]>(`${BASE_API_URL}/departments`, { | |||
next: { tags: ["departments"] }, | |||
}); | |||
}); |
@@ -0,0 +1,18 @@ | |||
"use server" | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
export interface CreatePositionInputs { | |||
positionCode: string; | |||
positionName: string; | |||
description: string; | |||
} | |||
export const savePosition = async (data: CreatePositionInputs) => { | |||
return serverFetchJson(`${BASE_API_URL}/positions/new`, { | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; |
@@ -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 PositionResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
description: string; | |||
} | |||
export const preloadPositions = () => { | |||
fetchPositions(); | |||
}; | |||
export const fetchPositions = cache(async () => { | |||
return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions`, { | |||
next: { tags: ["positions"] }, | |||
}); | |||
}); |
@@ -15,7 +15,12 @@ const pathToLabelMap: { [path: string]: string } = { | |||
"/tasks/create": "Create Task Template", | |||
"/customer": "Customer", | |||
"/customer/create": "Create Customer", | |||
"/settings": "Settings", | |||
"/company": "Company", | |||
"/settings/department": "Department", | |||
"/settings/department/new": "Create Department", | |||
"/settings/position": "Position", | |||
"/settings/position/new": "Create Position", | |||
}; | |||
const Breadcrumb = () => { | |||
@@ -0,0 +1,102 @@ | |||
"use client"; | |||
import Check from "@mui/icons-material/Check"; | |||
import Close from "@mui/icons-material/Close"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Tab from "@mui/material/Tab"; | |||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||
import { useRouter } from "next/navigation"; | |||
import React, { useCallback, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Task, TaskTemplate } from "@/app/api/tasks"; | |||
import { | |||
FieldErrors, | |||
FormProvider, | |||
SubmitErrorHandler, | |||
SubmitHandler, | |||
useForm, | |||
} from "react-hook-form"; | |||
import { CreateDepartmentInputs, saveDepartment } from "@/app/api/departments/actions"; | |||
import { Error } from "@mui/icons-material"; | |||
import { ProjectCategory } from "@/app/api/projects"; | |||
import { Staff } from "@/app/api/staff"; | |||
import { Typography } from "@mui/material"; | |||
import DepartmentDetails from "./DepartmentDetails"; | |||
const CreateDepartment: React.FC = ({ | |||
// allTasks, | |||
// projectCategories, | |||
// taskTemplates, | |||
// teamLeads, | |||
}) => { | |||
const [serverError, setServerError] = useState(""); | |||
const { t } = useTranslation(); | |||
const router = useRouter(); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
const onSubmit = useCallback<SubmitHandler<CreateDepartmentInputs>>( | |||
async (data) => { | |||
try { | |||
console.log(data); | |||
setServerError(""); | |||
// console.log(JSON.stringify(data)); | |||
await saveDepartment(data) | |||
router.replace("/settings/department"); | |||
} catch (e) { | |||
setServerError(t("An error has occurred. Please try again later.")); | |||
} | |||
}, | |||
[router, t], | |||
); | |||
const onSubmitError = useCallback<SubmitErrorHandler<CreateDepartmentInputs>>( | |||
(errors) => { | |||
console.log(errors) | |||
}, | |||
[], | |||
); | |||
const formProps = useForm<CreateDepartmentInputs>({ | |||
defaultValues: { | |||
departmentCode: "", | |||
departmentName: "", | |||
description: "", | |||
}, | |||
}); | |||
const errors = formProps.formState.errors; | |||
return ( | |||
<FormProvider {...formProps}> | |||
<Stack | |||
spacing={2} | |||
component="form" | |||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
> | |||
{ | |||
<DepartmentDetails /> | |||
} | |||
<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"> | |||
{t("Confirm")} | |||
</Button> | |||
</Stack> | |||
</Stack> | |||
</FormProvider> | |||
); | |||
}; | |||
export default CreateDepartment; |
@@ -0,0 +1,20 @@ | |||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | |||
import CreateDepartment from "./CreateDepartment"; | |||
import { fetchTeamLeads } from "@/app/api/staff"; | |||
const CreateDepartmentWrapper: React.FC = async () => { | |||
// const [tasks, taskTemplates, DepartmentCategories, teamLeads] = | |||
// await Promise.all([ | |||
// fetchAllTasks(), | |||
// fetchTaskTemplates(), | |||
// fetchDepartmentCategories(), | |||
// fetchTeamLeads(), | |||
// ]); | |||
return ( | |||
<CreateDepartment | |||
/> | |||
); | |||
}; | |||
export default CreateDepartmentWrapper; |
@@ -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 FormControl from "@mui/material/FormControl"; | |||
import Grid from "@mui/material/Grid"; | |||
import InputLabel from "@mui/material/InputLabel"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
import Select from "@mui/material/Select"; | |||
import TextField from "@mui/material/TextField"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import Button from "@mui/material/Button"; | |||
import { Controller, useFormContext } from "react-hook-form"; | |||
import { CreateDepartmentInputs } from "@/app/api/departments/actions"; | |||
import { Staff } from "@/app/api/staff"; | |||
const DepartmentDetails: React.FC = ({ | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { | |||
register, | |||
formState: { errors }, | |||
control, | |||
} = useFormContext<CreateDepartmentInputs>(); | |||
return ( | |||
<Card> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Department Details")} | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Department Code")} | |||
fullWidth | |||
{...register("departmentCode", { | |||
required: "Department code required!", | |||
})} | |||
error={Boolean(errors.departmentCode)} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Department Name")} | |||
fullWidth | |||
{...register("departmentName", { | |||
required: "Department name required!", | |||
})} | |||
error={Boolean(errors.departmentName)} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Department Description")} | |||
fullWidth | |||
{...register("description", { | |||
required: "Please enter a description", | |||
})} | |||
error={Boolean(errors.description)} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default DepartmentDetails; |
@@ -0,0 +1 @@ | |||
export { default } from "./CreateDepartmentWrapper" |
@@ -0,0 +1,102 @@ | |||
"use client"; | |||
import Check from "@mui/icons-material/Check"; | |||
import Close from "@mui/icons-material/Close"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Tab from "@mui/material/Tab"; | |||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||
import { useRouter } from "next/navigation"; | |||
import React, { useCallback, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Task, TaskTemplate } from "@/app/api/tasks"; | |||
import { | |||
FieldErrors, | |||
FormProvider, | |||
SubmitErrorHandler, | |||
SubmitHandler, | |||
useForm, | |||
} from "react-hook-form"; | |||
import { CreatePositionInputs, savePosition } from "@/app/api/positions/actions"; | |||
import { Error } from "@mui/icons-material"; | |||
import { ProjectCategory } from "@/app/api/projects"; | |||
import { Staff } from "@/app/api/staff"; | |||
import { Typography } from "@mui/material"; | |||
import PositionDetails from "./PositionDetails"; | |||
const CreatePosition: React.FC = ({ | |||
// allTasks, | |||
// projectCategories, | |||
// taskTemplates, | |||
// teamLeads, | |||
}) => { | |||
const [serverError, setServerError] = useState(""); | |||
const { t } = useTranslation(); | |||
const router = useRouter(); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
const onSubmit = useCallback<SubmitHandler<CreatePositionInputs>>( | |||
async (data) => { | |||
try { | |||
console.log(data); | |||
setServerError(""); | |||
// console.log(JSON.stringify(data)); | |||
await savePosition(data) | |||
router.replace("/settings/position"); | |||
} catch (e) { | |||
setServerError(t("An error has occurred. Please try again later.")); | |||
} | |||
}, | |||
[router, t], | |||
); | |||
const onSubmitError = useCallback<SubmitErrorHandler<CreatePositionInputs>>( | |||
(errors) => { | |||
console.log(errors) | |||
}, | |||
[], | |||
); | |||
const formProps = useForm<CreatePositionInputs>({ | |||
defaultValues: { | |||
positionCode: "", | |||
positionName: "", | |||
description: "", | |||
}, | |||
}); | |||
const errors = formProps.formState.errors; | |||
return ( | |||
<FormProvider {...formProps}> | |||
<Stack | |||
spacing={2} | |||
component="form" | |||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
> | |||
{ | |||
<PositionDetails /> | |||
} | |||
<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"> | |||
{t("Confirm")} | |||
</Button> | |||
</Stack> | |||
</Stack> | |||
</FormProvider> | |||
); | |||
}; | |||
export default CreatePosition; |
@@ -0,0 +1,18 @@ | |||
import CreatePosition from "./CreatePosition"; | |||
const CreatePositionWrapper: React.FC = async () => { | |||
// const [tasks, taskTemplates, PositionCategories, teamLeads] = | |||
// await Promise.all([ | |||
// fetchAllTasks(), | |||
// fetchTaskTemplates(), | |||
// fetchPositionCategories(), | |||
// fetchTeamLeads(), | |||
// ]); | |||
return ( | |||
<CreatePosition | |||
/> | |||
); | |||
}; | |||
export default CreatePositionWrapper; |
@@ -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 FormControl from "@mui/material/FormControl"; | |||
import Grid from "@mui/material/Grid"; | |||
import InputLabel from "@mui/material/InputLabel"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
import Select from "@mui/material/Select"; | |||
import TextField from "@mui/material/TextField"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import Button from "@mui/material/Button"; | |||
import { Controller, useFormContext } from "react-hook-form"; | |||
import { CreatePositionInputs } from "@/app/api/positions/actions"; | |||
import { Staff } from "@/app/api/staff"; | |||
const PositionDetails: React.FC = ({ | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { | |||
register, | |||
formState: { errors }, | |||
control, | |||
} = useFormContext<CreatePositionInputs>(); | |||
return ( | |||
<Card> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Position Details")} | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Position Code")} | |||
fullWidth | |||
{...register("positionCode", { | |||
required: "Position code required!", | |||
})} | |||
error={Boolean(errors.positionCode)} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Position Name")} | |||
fullWidth | |||
{...register("positionName", { | |||
required: "Position name required!", | |||
})} | |||
error={Boolean(errors.positionName)} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Position Description")} | |||
fullWidth | |||
{...register("description", { | |||
required: "Please enter a description", | |||
})} | |||
error={Boolean(errors.description)} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default PositionDetails; |
@@ -0,0 +1 @@ | |||
export { default } from "./CreatePositionWrapper" |
@@ -0,0 +1,82 @@ | |||
"use client"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
import EditNote from "@mui/icons-material/EditNote"; | |||
import uniq from "lodash/uniq"; | |||
import { DepartmentResult } from "@/app/api/departments"; | |||
interface Props { | |||
departments: DepartmentResult[]; | |||
} | |||
type SearchQuery = Partial<Omit<DepartmentResult, "id">>; | |||
type SearchParamNames = keyof SearchQuery; | |||
const DepartmentSearch: React.FC<Props> = ({ departments }) => { | |||
const { t } = useTranslation("departments"); | |||
const [filteredDepartments, setFilteredDepartments] = useState(departments); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
{ label: t("Department code"), paramName: "code", type: "text" }, | |||
{ label: t("Department name"), paramName: "name", type: "text" }, | |||
{ label: t("Department Description"), paramName: "description", type: "text" }, | |||
], | |||
[t, departments], | |||
); | |||
const onReset = useCallback(() => { | |||
setFilteredDepartments(departments); | |||
}, [departments]); | |||
const onProjectClick = useCallback((project: DepartmentResult) => { | |||
console.log(project); | |||
}, []); | |||
const columns = useMemo<Column<DepartmentResult>[]>( | |||
() => [ | |||
{ | |||
name: "id", | |||
label: t("Details"), | |||
onClick: onProjectClick, | |||
buttonIcon: <EditNote />, | |||
}, | |||
{ name: "code", label: t("Department Code") }, | |||
{ name: "name", label: t("Department Name") }, | |||
{ name: "description", label: t("Department Description") }, | |||
], | |||
[t, onProjectClick], | |||
); | |||
return ( | |||
<> | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
setFilteredDepartments( | |||
departments.filter( | |||
(d) => | |||
d.code.toLowerCase().includes(query.code.toLowerCase()) && | |||
d.name.toLowerCase().includes(query.name.toLowerCase()) && | |||
d.description.toLowerCase().includes(query.description.toLowerCase()) && | |||
{/*(query.client === "All" || p.client === query.client) && | |||
(query.category === "All" || p.category === query.category) && | |||
(query.team === "All" || p.team === query.team), **/} | |||
), | |||
); | |||
}} | |||
onReset={onReset} | |||
/> | |||
<SearchResults<DepartmentResult> | |||
items={filteredDepartments} | |||
columns={columns} | |||
/> | |||
</> | |||
); | |||
}; | |||
export default DepartmentSearch; |
@@ -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 DepartmentSearchLoading: 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>Department | |||
<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 DepartmentSearchLoading; |
@@ -0,0 +1,20 @@ | |||
// import { fetchDepartmentCategories, fetchDepartments } from "@/app/api/companys"; | |||
import React from "react"; | |||
import DepartmentSearch from "./DepartmentSearch"; | |||
import DepartmentSearchLoading from "./DepartmentSearchLoading"; | |||
import { fetchDepartments } from "@/app/api/departments"; | |||
interface SubComponents { | |||
Loading: typeof DepartmentSearchLoading; | |||
} | |||
const DepartmentSearchWrapper: React.FC & SubComponents = async () => { | |||
const Departments = await fetchDepartments(); | |||
// const Departments:any[] = [] | |||
return <DepartmentSearch departments={Departments} />; | |||
}; | |||
DepartmentSearchWrapper.Loading = DepartmentSearchLoading; | |||
export default DepartmentSearchWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./DepartmentSearchWrapper"; |
@@ -18,6 +18,9 @@ import Settings from "@mui/icons-material/Settings"; | |||
import Analytics from "@mui/icons-material/Analytics"; | |||
import Payments from "@mui/icons-material/Payments"; | |||
import Staff from "@mui/icons-material/PeopleAlt"; | |||
import Company from '@mui/icons-material/Store'; | |||
import Department from '@mui/icons-material/Diversity3'; | |||
import Position from '@mui/icons-material/Paragliding'; | |||
import { useTranslation } from "react-i18next"; | |||
import Typography from "@mui/material/Typography"; | |||
import { usePathname } from "next/navigation"; | |||
@@ -92,7 +95,9 @@ const navigationItems: NavigationItem[] = [ | |||
children: [ | |||
{ icon: <GroupIcon />, label: "Customer", path: "/customer" }, | |||
{ icon: <Staff />, label: "Staff", path: "/staff" }, | |||
{ icon: <Staff />, label: "Company", path: "/settings/company" } | |||
{ icon: <Company />, label: "Company", path: "/settings/company" }, | |||
{ icon: <Department />, label: "Department", path: "/settings/department" }, | |||
{ icon: <Position />, label: "Position", path: "/settings/position" }, | |||
], | |||
}, | |||
]; | |||
@@ -0,0 +1,81 @@ | |||
"use client"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
import EditNote from "@mui/icons-material/EditNote"; | |||
import { PositionResult } from "@/app/api/positions"; | |||
interface Props { | |||
positions: PositionResult[]; | |||
} | |||
type SearchQuery = Partial<Omit<PositionResult, "id">>; | |||
type SearchParamNames = keyof SearchQuery; | |||
const PositionSearch: React.FC<Props> = ({ positions }) => { | |||
const { t } = useTranslation("positions"); | |||
const [filteredPositions, setFilteredPositions] = useState(positions); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
{ label: t("Position code"), paramName: "code", type: "text" }, | |||
{ label: t("Position name"), paramName: "name", type: "text" }, | |||
{ label: t("Position Description"), paramName: "description", type: "text" }, | |||
], | |||
[t, positions], | |||
); | |||
const onReset = useCallback(() => { | |||
setFilteredPositions(positions); | |||
}, [positions]); | |||
const onProjectClick = useCallback((project: PositionResult) => { | |||
console.log(project); | |||
}, []); | |||
const columns = useMemo<Column<PositionResult>[]>( | |||
() => [ | |||
{ | |||
name: "id", | |||
label: t("Details"), | |||
onClick: onProjectClick, | |||
buttonIcon: <EditNote />, | |||
}, | |||
{ name: "code", label: t("Position Code") }, | |||
{ name: "name", label: t("Position Name") }, | |||
{ name: "description", label: t("Position Description") }, | |||
], | |||
[t, onProjectClick], | |||
); | |||
return ( | |||
<> | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
setFilteredPositions( | |||
positions.filter( | |||
(d) => | |||
d.code.toLowerCase().includes(query.code.toLowerCase()) && | |||
d.name.toLowerCase().includes(query.name.toLowerCase()) && | |||
d.description.toLowerCase().includes(query.description.toLowerCase()) && | |||
{/*(query.client === "All" || p.client === query.client) && | |||
(query.category === "All" || p.category === query.category) && | |||
(query.team === "All" || p.team === query.team), **/} | |||
), | |||
); | |||
}} | |||
onReset={onReset} | |||
/> | |||
<SearchResults<PositionResult> | |||
items={filteredPositions} | |||
columns={columns} | |||
/> | |||
</> | |||
); | |||
}; | |||
export default PositionSearch; |
@@ -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 PositionSearchLoading: 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>Position | |||
<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 PositionSearchLoading; |
@@ -0,0 +1,20 @@ | |||
// import { fetchPositionCategories, fetchPositions } from "@/app/api/companys"; | |||
import React from "react"; | |||
import PositionSearch from "./PositionSearch"; | |||
import PositionSearchLoading from "./PositionSearchLoading"; | |||
import { fetchPositions } from "@/app/api/positions"; | |||
interface SubComponents { | |||
Loading: typeof PositionSearchLoading; | |||
} | |||
const PositionSearchWrapper: React.FC & SubComponents = async () => { | |||
const Positions = await fetchPositions(); | |||
// const Positions:any[] = [] | |||
return <PositionSearch positions={Positions} />; | |||
}; | |||
PositionSearchWrapper.Loading = PositionSearchLoading; | |||
export default PositionSearchWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./PositionSearchWrapper"; |