Reviewed-on: https://git.2fi-solutions.com/wayne.lee/tsms/pulls/4tags/Baseline_30082024_FRONTEND_UAT
@@ -27,7 +27,7 @@ export default async function MainLayout({ | |||||
<Box | <Box | ||||
component="main" | component="main" | ||||
sx={{ | sx={{ | ||||
marginInlineStart: { xs: 0, lg: NAVIGATION_CONTENT_WIDTH }, | |||||
marginInlineStart: { xs: 0, xl: NAVIGATION_CONTENT_WIDTH }, | |||||
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | ||||
}} | }} | ||||
> | > | ||||
@@ -1,6 +1,5 @@ | |||||
import { preloadProjects } from "@/app/api/projects"; | import { preloadProjects } from "@/app/api/projects"; | ||||
import ProjectSearch from "@/components/ProjectSearch"; | import ProjectSearch from "@/components/ProjectSearch"; | ||||
import ProgressByClientSearch from "@/components/ProgressByClientSearch"; | |||||
import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
@@ -1,3 +1,4 @@ | |||||
import { preloadAllTasks } from "@/app/api/tasks"; | |||||
import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | ||||
import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
@@ -9,6 +10,7 @@ export const metadata: Metadata = { | |||||
const Projects: React.FC = async () => { | const Projects: React.FC = async () => { | ||||
const { t } = await getServerI18n("tasks"); | const { t } = await getServerI18n("tasks"); | ||||
preloadAllTasks(); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -0,0 +1,19 @@ | |||||
"use server"; | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { TaskTemplate } from "."; | |||||
export interface NewTaskTemplateFormInputs { | |||||
code: string; | |||||
name: string; | |||||
taskIds: number[]; | |||||
} | |||||
export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { | |||||
return serverFetchJson<TaskTemplate>(`${BASE_API_URL}/tasks/templates/new`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; |
@@ -1,7 +1,21 @@ | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
export interface TaskTemplateResult { | |||||
export interface TaskGroup { | |||||
id: number; | |||||
name: string; | |||||
} | |||||
export interface Task { | |||||
id: number; | |||||
name: string; | |||||
description: string | null; | |||||
taskGroup: TaskGroup | null; | |||||
} | |||||
export interface TaskTemplate { | |||||
id: number; | id: number; | ||||
code: string; | code: string; | ||||
name: string; | name: string; | ||||
@@ -12,23 +26,13 @@ export const preloadTaskTemplates = () => { | |||||
}; | }; | ||||
export const fetchTaskTemplates = cache(async () => { | export const fetchTaskTemplates = cache(async () => { | ||||
return mockProjects; | |||||
return serverFetchJson<TaskTemplate[]>(`${BASE_API_URL}/tasks/templates`); | |||||
}); | }); | ||||
const mockProjects: TaskTemplateResult[] = [ | |||||
{ | |||||
id: 1, | |||||
code: "Pre-001", | |||||
name: "Pre-contract Template", | |||||
}, | |||||
{ | |||||
id: 2, | |||||
code: "Post-001", | |||||
name: "Post-contract Template", | |||||
}, | |||||
{ | |||||
id: 3, | |||||
code: "Full-001", | |||||
name: "Full Project Template", | |||||
}, | |||||
]; | |||||
export const preloadAllTasks = () => { | |||||
fetchAllTasks(); | |||||
}; | |||||
export const fetchAllTasks = cache(async () => { | |||||
return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | |||||
}); |
@@ -0,0 +1,7 @@ | |||||
import LogoutPage from "@/components/LogoutPage"; | |||||
const Logout: React.FC = async () => { | |||||
return <LogoutPage />; | |||||
}; | |||||
export default Logout; |
@@ -0,0 +1,46 @@ | |||||
import { SessionWithTokens, authOptions } from "@/config/authConfig"; | |||||
import { getServerSession } from "next-auth"; | |||||
import { headers } from "next/headers"; | |||||
import { redirect } from "next/navigation"; | |||||
export const serverFetch: typeof fetch = async (input, init) => { | |||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |||||
const session = await getServerSession<any, SessionWithTokens>(authOptions); | |||||
const accessToken = session?.accessToken; | |||||
return fetch(input, { | |||||
...init, | |||||
headers: { | |||||
...init?.headers, | |||||
...(accessToken | |||||
? { | |||||
Authorization: `Bearer ${accessToken}`, | |||||
} | |||||
: {}), | |||||
}, | |||||
}); | |||||
}; | |||||
type FetchParams = Parameters<typeof fetch>; | |||||
export async function serverFetchJson<T>(...args: FetchParams) { | |||||
const response = await serverFetch(...args); | |||||
if (response.ok) { | |||||
return response.json() as T; | |||||
} else { | |||||
switch (response.status) { | |||||
case 401: | |||||
signOutUser(); | |||||
default: | |||||
throw Error("Something went wrong fetching data in server."); | |||||
} | |||||
} | |||||
} | |||||
export const signOutUser = () => { | |||||
const headersList = headers(); | |||||
const referer = headersList.get("referer"); | |||||
redirect( | |||||
`/logout${referer ? `?callbackUrl=${encodeURIComponent(referer)}` : ""}`, | |||||
); | |||||
}; |
@@ -18,7 +18,7 @@ const NavigationToggle: React.FC = () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Drawer variant="permanent" sx={{ display: { xs: "none", xl: "block" } }}> | <Drawer variant="permanent" sx={{ display: { xs: "none", xl: "block" } }}> | ||||
<NavigationContent/> | |||||
<NavigationContent /> | |||||
</Drawer> | </Drawer> | ||||
<Drawer | <Drawer | ||||
sx={{ display: { xl: "none" } }} | sx={{ display: { xl: "none" } }} | ||||
@@ -28,7 +28,7 @@ const NavigationToggle: React.FC = () => { | |||||
keepMounted: true, | keepMounted: true, | ||||
}} | }} | ||||
> | > | ||||
<NavigationContent/> | |||||
<NavigationContent /> | |||||
</Drawer> | </Drawer> | ||||
<IconButton | <IconButton | ||||
sx={{ display: { xl: "none" } }} | sx={{ display: { xl: "none" } }} | ||||
@@ -11,6 +11,8 @@ import React, { useCallback, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import ProjectClientDetails from "./ProjectClientDetails"; | import ProjectClientDetails from "./ProjectClientDetails"; | ||||
import TaskSetup from "./TaskSetup"; | import TaskSetup from "./TaskSetup"; | ||||
import StaffAllocation from "./StaffAllocation"; | |||||
import ResourceMilestone from "./ResourceMilestone"; | |||||
const CreateProject: React.FC = () => { | const CreateProject: React.FC = () => { | ||||
const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
@@ -38,6 +40,8 @@ const CreateProject: React.FC = () => { | |||||
</Tabs> | </Tabs> | ||||
{tabIndex === 0 && <ProjectClientDetails />} | {tabIndex === 0 && <ProjectClientDetails />} | ||||
{tabIndex === 1 && <TaskSetup />} | {tabIndex === 1 && <TaskSetup />} | ||||
{tabIndex === 2 && <StaffAllocation initiallySelectedStaff={[]} />} | |||||
{tabIndex === 3 && <ResourceMilestone />} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | <Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | ||||
{t("Cancel")} | {t("Cancel")} | ||||
@@ -0,0 +1,31 @@ | |||||
"use client"; | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import Button from "@mui/material/Button"; | |||||
import React from "react"; | |||||
import CardActions from "@mui/material/CardActions"; | |||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
const ResourceMilestone = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<Card> | |||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Resource and Milestone")} | |||||
</Typography> | |||||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||||
<Button variant="text" startIcon={<RestartAlt />}> | |||||
{t("Reset")} | |||||
</Button> | |||||
</CardActions> | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default ResourceMilestone; |
@@ -0,0 +1,252 @@ | |||||
"use client"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import React from "react"; | |||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; | |||||
import { | |||||
Stack, | |||||
Typography, | |||||
Grid, | |||||
TextField, | |||||
InputAdornment, | |||||
IconButton, | |||||
FormControl, | |||||
InputLabel, | |||||
Select, | |||||
MenuItem, | |||||
Box, | |||||
Button, | |||||
Card, | |||||
CardActions, | |||||
CardContent, | |||||
TabsProps, | |||||
Tab, | |||||
Tabs, | |||||
} from "@mui/material"; | |||||
import differenceBy from "lodash/differenceBy"; | |||||
interface StaffResult { | |||||
id: string; | |||||
name: string; | |||||
team: string; | |||||
grade: string; | |||||
title: string; | |||||
} | |||||
const mockStaffs: StaffResult[] = [ | |||||
{ | |||||
name: "Albert", | |||||
grade: "1", | |||||
id: "1", | |||||
team: "ABC", | |||||
title: "Associate Quantity Surveyor", | |||||
}, | |||||
{ | |||||
name: "Bernard", | |||||
grade: "2", | |||||
id: "2", | |||||
team: "ABC", | |||||
title: "Quantity Surveyor", | |||||
}, | |||||
{ | |||||
name: "Carl", | |||||
grade: "3", | |||||
id: "3", | |||||
team: "XYZ", | |||||
title: "Senior Quantity Surveyor", | |||||
}, | |||||
{ name: "Denis", grade: "4", id: "4", team: "ABC", title: "Manager" }, | |||||
{ name: "Edward", grade: "5", id: "5", team: "ABC", title: "Director" }, | |||||
{ name: "Fred", grade: "1", id: "6", team: "XYZ", title: "General Laborer" }, | |||||
{ name: "Gordon", grade: "2", id: "7", team: "ABC", title: "Inspector" }, | |||||
{ | |||||
name: "Heather", | |||||
grade: "3", | |||||
id: "8", | |||||
team: "XYZ", | |||||
title: "Field Engineer", | |||||
}, | |||||
{ name: "Ivan", grade: "4", id: "9", team: "ABC", title: "Senior Manager" }, | |||||
{ | |||||
name: "Jackson", | |||||
grade: "5", | |||||
id: "10", | |||||
team: "XYZ", | |||||
title: "Senior Director", | |||||
}, | |||||
{ | |||||
name: "Kurt", | |||||
grade: "1", | |||||
id: "11", | |||||
team: "ABC", | |||||
title: "Construction Assistant", | |||||
}, | |||||
{ name: "Lawrence", grade: "2", id: "12", team: "ABC", title: "Operator" }, | |||||
]; | |||||
interface Props { | |||||
allStaff?: StaffResult[]; | |||||
initiallySelectedStaff: StaffResult[]; | |||||
} | |||||
const StaffAllocation: React.FC<Props> = ({ | |||||
allStaff = mockStaffs, | |||||
initiallySelectedStaff, | |||||
}) => { | |||||
const { t } = useTranslation(); | |||||
const [filteredStaff, setFilteredStaff] = React.useState(allStaff); | |||||
const [selectedStaff, setSelectedStaff] = React.useState< | |||||
typeof filteredStaff | |||||
>(initiallySelectedStaff); | |||||
const filters = React.useMemo<(keyof StaffResult)[]>( | |||||
() => ["team", "grade"], | |||||
[], | |||||
); | |||||
const addStaff = React.useCallback((staff: StaffResult) => { | |||||
setSelectedStaff((staffs) => [...staffs, staff]); | |||||
}, []); | |||||
const removeStaff = React.useCallback((staff: StaffResult) => { | |||||
setSelectedStaff((staffs) => staffs.filter((s) => s.id !== staff.id)); | |||||
}, []); | |||||
const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Add"), | |||||
name: "id", | |||||
onClick: addStaff, | |||||
buttonIcon: <PersonAdd />, | |||||
}, | |||||
{ label: t("Staff ID"), name: "id" }, | |||||
{ label: t("Staff Name"), name: "name" }, | |||||
{ label: t("Team"), name: "team" }, | |||||
{ label: t("Grade"), name: "grade" }, | |||||
{ label: t("Title"), name: "title" }, | |||||
], | |||||
[addStaff, t], | |||||
); | |||||
const allocatedStaffColumns = React.useMemo<Column<StaffResult>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Remove"), | |||||
name: "id", | |||||
onClick: removeStaff, | |||||
buttonIcon: <PersonRemove />, | |||||
}, | |||||
{ label: t("Staff ID"), name: "id" }, | |||||
{ label: t("Staff Name"), name: "name" }, | |||||
{ label: t("Team"), name: "team" }, | |||||
{ label: t("Grade"), name: "grade" }, | |||||
{ label: t("Title"), name: "title" }, | |||||
], | |||||
[removeStaff, 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( | |||||
allStaff.filter( | |||||
(staff) => | |||||
staff.name.toLowerCase().includes(query) || | |||||
staff.id.toLowerCase().includes(query) || | |||||
staff.title.toLowerCase().includes(query), | |||||
), | |||||
); | |||||
}, [allStaff, query]); | |||||
const [tabIndex, setTabIndex] = React.useState(0); | |||||
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[], | |||||
); | |||||
return ( | |||||
<Card> | |||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
<Stack gap={2}> | |||||
<Typography variant="overline" display="block"> | |||||
{t("Staff Allocation")} | |||||
</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 title")} | |||||
InputProps={{ | |||||
endAdornment: query && ( | |||||
<InputAdornment position="end"> | |||||
<IconButton onClick={clearQueryInput}> | |||||
<Clear /> | |||||
</IconButton> | |||||
</InputAdornment> | |||||
), | |||||
}} | |||||
/> | |||||
</Grid> | |||||
{filters.map((filter, idx) => { | |||||
const label = staffPoolColumns.find( | |||||
(c) => c.name === filter, | |||||
)!.label; | |||||
return ( | |||||
<Grid key={`${filter.toString()}-${idx}`} item xs={3}> | |||||
<FormControl fullWidth> | |||||
<InputLabel size="small">{label}</InputLabel> | |||||
<Select label={label} size="small"> | |||||
<MenuItem value={"All"}>{t("All")}</MenuItem> | |||||
</Select> | |||||
</FormControl> | |||||
</Grid> | |||||
); | |||||
})} | |||||
</Grid> | |||||
<Tabs value={tabIndex} onChange={handleTabChange}> | |||||
<Tab label={t("Staff Pool")} /> | |||||
<Tab label={t("Allocated Staff")} /> | |||||
</Tabs> | |||||
<Box sx={{ marginInline: -3 }}> | |||||
{tabIndex === 0 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={differenceBy(filteredStaff, selectedStaff, "id")} | |||||
columns={staffPoolColumns} | |||||
/> | |||||
)} | |||||
{tabIndex === 1 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={selectedStaff} | |||||
columns={allocatedStaffColumns} | |||||
/> | |||||
)} | |||||
</Box> | |||||
</Stack> | |||||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||||
<Button variant="text" startIcon={<RestartAlt />}> | |||||
{t("Reset")} | |||||
</Button> | |||||
</CardActions> | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default StaffAllocation; |
@@ -13,8 +13,18 @@ import Close from "@mui/icons-material/Close"; | |||||
import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
import React from "react"; | import React from "react"; | ||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import { Task } from "@/app/api/tasks"; | |||||
import { | |||||
NewTaskTemplateFormInputs, | |||||
saveTaskTemplate, | |||||
} from "@/app/api/tasks/actions"; | |||||
import { SubmitHandler, useForm } from "react-hook-form"; | |||||
const CreateTaskTemplate = () => { | |||||
interface Props { | |||||
tasks: Task[]; | |||||
} | |||||
const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
@@ -22,8 +32,40 @@ const CreateTaskTemplate = () => { | |||||
router.back(); | router.back(); | ||||
}; | }; | ||||
const items = React.useMemo( | |||||
() => | |||||
tasks.map((task) => ({ | |||||
id: task.id, | |||||
label: task.name, | |||||
group: task.taskGroup || undefined, | |||||
})), | |||||
[tasks], | |||||
); | |||||
const [serverError, setServerError] = React.useState(""); | |||||
const { | |||||
register, | |||||
handleSubmit, | |||||
setValue, | |||||
formState: { errors, isSubmitting }, | |||||
} = useForm<NewTaskTemplateFormInputs>(); | |||||
const onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback( | |||||
async (data) => { | |||||
try { | |||||
setServerError(""); | |||||
await saveTaskTemplate(data); | |||||
router.replace("/tasks"); | |||||
} catch (e) { | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
}, | |||||
[router, t], | |||||
); | |||||
return ( | return ( | ||||
<> | |||||
<Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}> | |||||
<Card> | <Card> | ||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
<Typography variant="overline">{t("Task List Setup")}</Typography> | <Typography variant="overline">{t("Task List Setup")}</Typography> | ||||
@@ -34,40 +76,61 @@ const CreateTaskTemplate = () => { | |||||
marginBlockEnd={1} | marginBlockEnd={1} | ||||
> | > | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField label={t("Task Template Code")} fullWidth /> | |||||
<TextField | |||||
label={t("Task Template Code")} | |||||
fullWidth | |||||
{...register("code", { | |||||
required: t("Task template code is required"), | |||||
})} | |||||
error={Boolean(errors.code?.message)} | |||||
helperText={errors.code?.message} | |||||
/> | |||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField label={t("Task Template Name")} fullWidth /> | |||||
<TextField | |||||
label={t("Task Template Name")} | |||||
fullWidth | |||||
{...register("name", { | |||||
required: t("Task template name is required"), | |||||
})} | |||||
error={Boolean(errors.name?.message)} | |||||
helperText={errors.name?.message} | |||||
/> | |||||
</Grid> | </Grid> | ||||
</Grid> | </Grid> | ||||
<TransferList | <TransferList | ||||
allItems={[ | |||||
{ id: "1", label: "Task 1: Super long task name that will overflow to the next line" }, | |||||
{ id: "2", label: "Task 2" }, | |||||
{ id: "3", label: "Task 3" }, | |||||
{ id: "4", label: "Task 4" }, | |||||
{ id: "5", label: "Task 5" }, | |||||
{ id: "6", label: "Task 6" }, | |||||
{ id: "7", label: "Task 7" }, | |||||
{ id: "8", label: "Task 8" }, | |||||
{ id: "9", label: "Task 9" }, | |||||
]} | |||||
allItems={items} | |||||
initiallySelectedItems={[]} | initiallySelectedItems={[]} | ||||
onChange={() => {}} | |||||
onChange={(selectedItems) => { | |||||
setValue( | |||||
"taskIds", | |||||
selectedItems.map((item) => item.id), | |||||
); | |||||
}} | |||||
allItemsLabel={t("Task Pool")} | allItemsLabel={t("Task Pool")} | ||||
selectedItemsLabel={t("Task List Template")} | selectedItemsLabel={t("Task List Template")} | ||||
/> | /> | ||||
</CardContent> | </CardContent> | ||||
</Card> | </Card> | ||||
{serverError && ( | |||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
{serverError} | |||||
</Typography> | |||||
)} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | <Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | ||||
{t("Cancel")} | {t("Cancel")} | ||||
</Button> | </Button> | ||||
<Button variant="contained" startIcon={<Check />}> | |||||
<Button | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
disabled={isSubmitting} | |||||
> | |||||
{t("Confirm")} | {t("Confirm")} | ||||
</Button> | </Button> | ||||
</Stack> | </Stack> | ||||
</> | |||||
</Stack> | |||||
); | ); | ||||
}; | }; | ||||
@@ -0,0 +1,11 @@ | |||||
import React from "react"; | |||||
import CreateTaskTemplate from "./CreateTaskTemplate"; | |||||
import { fetchAllTasks } from "@/app/api/tasks"; | |||||
const CreateTaskTemplateWrapper: React.FC = async () => { | |||||
const tasks = await fetchAllTasks(); | |||||
return <CreateTaskTemplate tasks={tasks} />; | |||||
}; | |||||
export default CreateTaskTemplateWrapper; |
@@ -1 +1 @@ | |||||
export { default } from "./CreateTaskTemplate"; | |||||
export { default } from "./CreateTaskTemplateWrapper"; |
@@ -0,0 +1,17 @@ | |||||
"use client"; | |||||
import { signOut } from "next-auth/react"; | |||||
import { useSearchParams } from "next/navigation"; | |||||
import { useEffect } from "react"; | |||||
const LogoutPage = () => { | |||||
const params = useSearchParams(); | |||||
const callbackUrl = params.get("callbackUrl"); | |||||
useEffect(() => { | |||||
signOut({ redirect: true, callbackUrl: callbackUrl || "/" }); | |||||
}); | |||||
return null; | |||||
}; | |||||
export default LogoutPage; |
@@ -0,0 +1 @@ | |||||
export { default } from "./LogoutPage"; |
@@ -1,10 +1,11 @@ | |||||
"use client"; | "use client"; | ||||
import { ProjectResult } from "@/app/api/projects"; | import { ProjectResult } from "@/app/api/projects"; | ||||
import React, { useMemo, useState } from "react"; | |||||
import React, { useCallback, useMemo, useState } from "react"; | |||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
import EditNote from "@mui/icons-material/EditNote"; | |||||
interface Props { | interface Props { | ||||
projects: ProjectResult[]; | projects: ProjectResult[]; | ||||
@@ -45,16 +46,25 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||||
[t], | [t], | ||||
); | ); | ||||
const onProjectClick = useCallback((project: ProjectResult) => { | |||||
console.log(project); | |||||
}, []); | |||||
const columns = useMemo<Column<ProjectResult>[]>( | const columns = useMemo<Column<ProjectResult>[]>( | ||||
() => [ | () => [ | ||||
{ name: "id", label: t("Details") }, | |||||
{ | |||||
name: "id", | |||||
label: t("Details"), | |||||
onClick: onProjectClick, | |||||
buttonIcon: <EditNote />, | |||||
}, | |||||
{ name: "code", label: t("Project Code") }, | { name: "code", label: t("Project Code") }, | ||||
{ name: "name", label: t("Project Name") }, | { name: "name", label: t("Project Name") }, | ||||
{ name: "category", label: t("Project Category") }, | { name: "category", label: t("Project Category") }, | ||||
{ name: "team", label: t("Team") }, | { name: "team", label: t("Team") }, | ||||
{ name: "client", label: t("Client") }, | { name: "client", label: t("Client") }, | ||||
], | ], | ||||
[t], | |||||
[t, onProjectClick], | |||||
); | ); | ||||
return ( | return ( | ||||
@@ -35,9 +35,14 @@ export type Criterion<T extends string> = TextCriterion<T> | SelectCriterion<T>; | |||||
interface Props<T extends string> { | interface Props<T extends string> { | ||||
criteria: Criterion<T>[]; | criteria: Criterion<T>[]; | ||||
onSearch: (inputs: Record<T, string>) => void; | onSearch: (inputs: Record<T, string>) => void; | ||||
onReset?: () => void; | |||||
} | } | ||||
function SearchBox<T extends string>({ criteria, onSearch }: Props<T>) { | |||||
function SearchBox<T extends string>({ | |||||
criteria, | |||||
onSearch, | |||||
onReset, | |||||
}: Props<T>) { | |||||
const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
const defaultInputs = useMemo( | const defaultInputs = useMemo( | ||||
() => | () => | ||||
@@ -68,6 +73,7 @@ function SearchBox<T extends string>({ criteria, onSearch }: Props<T>) { | |||||
const handleReset = () => { | const handleReset = () => { | ||||
setInputs(defaultInputs); | setInputs(defaultInputs); | ||||
onReset?.(); | |||||
}; | }; | ||||
const handleSearch = () => { | const handleSearch = () => { | ||||
@@ -77,7 +83,7 @@ function SearchBox<T extends string>({ criteria, onSearch }: Props<T>) { | |||||
return ( | return ( | ||||
<Card> | <Card> | ||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
<Typography variant="overline" style={{fontWeight:"600"}}>{t("Search Criteria")}</Typography> | |||||
<Typography variant="overline">{t("Search Criteria")}</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
{criteria.map((c) => { | {criteria.map((c) => { | ||||
return ( | return ( | ||||
@@ -12,23 +12,42 @@ import TablePagination, { | |||||
} from "@mui/material/TablePagination"; | } from "@mui/material/TablePagination"; | ||||
import TableRow from "@mui/material/TableRow"; | import TableRow from "@mui/material/TableRow"; | ||||
import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||
import EditNote from "@mui/icons-material/EditNote"; | |||||
interface ResultWithId { | |||||
export interface ResultWithId { | |||||
id: string | number; | id: string | number; | ||||
} | } | ||||
export interface Column<T extends ResultWithId> { | |||||
interface BaseColumn<T extends ResultWithId> { | |||||
name: keyof T; | name: keyof T; | ||||
label: string; | label: string; | ||||
} | } | ||||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||||
onClick: (item: T) => void; | |||||
buttonIcon: React.ReactNode; | |||||
} | |||||
export type Column<T extends ResultWithId> = | |||||
| BaseColumn<T> | |||||
| ColumnWithAction<T>; | |||||
interface Props<T extends ResultWithId> { | interface Props<T extends ResultWithId> { | ||||
items: T[]; | items: T[]; | ||||
columns: Column<T>[]; | columns: Column<T>[]; | ||||
noWrapper?: boolean; | |||||
} | } | ||||
function SearchResults<T extends ResultWithId>({ items, columns }: Props<T>) { | |||||
function isActionColumn<T extends ResultWithId>( | |||||
column: Column<T>, | |||||
): column is ColumnWithAction<T> { | |||||
return Boolean((column as ColumnWithAction<T>).onClick); | |||||
} | |||||
function SearchResults<T extends ResultWithId>({ | |||||
items, | |||||
columns, | |||||
noWrapper, | |||||
}: Props<T>) { | |||||
const [page, setPage] = React.useState(0); | const [page, setPage] = React.useState(0); | ||||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | const [rowsPerPage, setRowsPerPage] = React.useState(10); | ||||
@@ -46,14 +65,14 @@ function SearchResults<T extends ResultWithId>({ items, columns }: Props<T>) { | |||||
setPage(0); | setPage(0); | ||||
}; | }; | ||||
return ( | |||||
<Paper sx={{ overflow: "hidden" }}> | |||||
const table = ( | |||||
<> | |||||
<TableContainer sx={{ maxHeight: 440 }}> | <TableContainer sx={{ maxHeight: 440 }}> | ||||
<Table stickyHeader> | <Table stickyHeader> | ||||
<TableHead> | <TableHead> | ||||
<TableRow> | <TableRow> | ||||
{columns.map((column) => ( | |||||
<TableCell key={column.name.toString()}> | |||||
{columns.map((column, idx) => ( | |||||
<TableCell key={`${column.name.toString()}${idx}`}> | |||||
{column.label} | {column.label} | ||||
</TableCell> | </TableCell> | ||||
))} | ))} | ||||
@@ -65,12 +84,17 @@ function SearchResults<T extends ResultWithId>({ items, columns }: Props<T>) { | |||||
.map((item) => { | .map((item) => { | ||||
return ( | return ( | ||||
<TableRow hover tabIndex={-1} key={item.id}> | <TableRow hover tabIndex={-1} key={item.id}> | ||||
{columns.map(({ name: columnName }) => { | |||||
{columns.map((column, idx) => { | |||||
const columnName = column.name; | |||||
return ( | return ( | ||||
<TableCell key={columnName.toString()}> | |||||
{columnName === "id" ? ( | |||||
<IconButton color="primary"> | |||||
<EditNote /> | |||||
<TableCell key={`${columnName.toString()}-${idx}`}> | |||||
{isActionColumn(column) ? ( | |||||
<IconButton | |||||
color="primary" | |||||
onClick={() => column.onClick(item)} | |||||
> | |||||
{column.buttonIcon} | |||||
</IconButton> | </IconButton> | ||||
) : ( | ) : ( | ||||
<>{item[columnName]}</> | <>{item[columnName]}</> | ||||
@@ -93,8 +117,10 @@ function SearchResults<T extends ResultWithId>({ items, columns }: Props<T>) { | |||||
onPageChange={handleChangePage} | onPageChange={handleChangePage} | ||||
onRowsPerPageChange={handleChangeRowsPerPage} | onRowsPerPageChange={handleChangeRowsPerPage} | ||||
/> | /> | ||||
</Paper> | |||||
</> | |||||
); | ); | ||||
return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>; | |||||
} | } | ||||
export default SearchResults; | export default SearchResults; |
@@ -1,24 +1,23 @@ | |||||
"use client"; | "use client"; | ||||
import { TaskTemplateResult } from "@/app/api/tasks"; | |||||
import React, { useMemo, useState } from "react"; | |||||
import { TaskTemplate } from "@/app/api/tasks"; | |||||
import React, { useCallback, useMemo, useState } from "react"; | |||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
import EditNote from "@mui/icons-material/EditNote"; | |||||
interface Props { | interface Props { | ||||
taskTemplates: TaskTemplateResult[]; | |||||
taskTemplates: TaskTemplate[]; | |||||
} | } | ||||
type SearchQuery = Partial<Omit<TaskTemplateResult, "id">>; | |||||
type SearchQuery = Partial<Omit<TaskTemplate, "id">>; | |||||
type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | ||||
const { t } = useTranslation("tasks"); | const { t } = useTranslation("tasks"); | ||||
// If task searching is done on the server-side, then no need for this. | |||||
const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); | const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); | ||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
() => [ | () => [ | ||||
{ label: t("Task Template Code"), paramName: "code", type: "text" }, | { label: t("Task Template Code"), paramName: "code", type: "text" }, | ||||
@@ -26,14 +25,26 @@ const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | |||||
], | ], | ||||
[t], | [t], | ||||
); | ); | ||||
const onReset = useCallback(() => { | |||||
setFilteredTemplates(taskTemplates); | |||||
}, [taskTemplates]); | |||||
const onTaskClick = useCallback((taskTemplate: TaskTemplate) => { | |||||
console.log(taskTemplate); | |||||
}, []); | |||||
const columns = useMemo<Column<TaskTemplateResult>[]>( | |||||
const columns = useMemo<Column<TaskTemplate>[]>( | |||||
() => [ | () => [ | ||||
{ name: "id", label: t("Details") }, | |||||
{ | |||||
name: "id", | |||||
label: t("Details"), | |||||
onClick: onTaskClick, | |||||
buttonIcon: <EditNote />, | |||||
}, | |||||
{ name: "code", label: t("Task Template Code") }, | { name: "code", label: t("Task Template Code") }, | ||||
{ name: "name", label: t("Task Template Name") }, | { name: "name", label: t("Task Template Name") }, | ||||
], | ], | ||||
[t], | |||||
[onTaskClick, t], | |||||
); | ); | ||||
return ( | return ( | ||||
@@ -41,8 +52,15 @@ const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | |||||
<SearchBox | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
console.log(query); | |||||
setFilteredTemplates( | |||||
taskTemplates.filter( | |||||
(task) => | |||||
task.code.toLowerCase().includes(query.code) && | |||||
task.name.toLowerCase().includes(query.name), | |||||
), | |||||
); | |||||
}} | }} | ||||
onReset={onReset} | |||||
/> | /> | ||||
<SearchResults items={filteredTemplates} columns={columns} /> | <SearchResults items={filteredTemplates} columns={columns} /> | ||||
</> | </> | ||||
@@ -15,8 +15,11 @@ import { | |||||
Stack, | Stack, | ||||
Typography, | Typography, | ||||
} from "@mui/material"; | } from "@mui/material"; | ||||
import React, { useCallback, useState } from "react"; | |||||
import { LabelWithId, TransferListProps } from "./TransferList"; | |||||
import React, { useCallback, useEffect, useState } from "react"; | |||||
import { LabelGroup, LabelWithId, TransferListProps } from "./TransferList"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import uniqBy from "lodash/uniqBy"; | |||||
import groupBy from "lodash/groupBy"; | |||||
export const MultiSelectList: React.FC<TransferListProps> = ({ | export const MultiSelectList: React.FC<TransferListProps> = ({ | ||||
allItems, | allItems, | ||||
@@ -33,7 +36,7 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||||
); | ); | ||||
}, [allItems]); | }, [allItems]); | ||||
const compareFn = React.useCallback( | const compareFn = React.useCallback( | ||||
(a: string, b: string) => sortMap[a].index - sortMap[b].index, | |||||
(a: number, b: number) => sortMap[a].index - sortMap[b].index, | |||||
[sortMap], | [sortMap], | ||||
); | ); | ||||
const [selectedItems, setSelectedItems] = useState( | const [selectedItems, setSelectedItems] = useState( | ||||
@@ -45,7 +48,7 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||||
const { | const { | ||||
target: { value }, | target: { value }, | ||||
} = event; | } = event; | ||||
setSelectedItems(typeof value === "string" ? [value] : value); | |||||
setSelectedItems(typeof value === "string" ? [Number(value)] : value); | |||||
}, []); | }, []); | ||||
const handleToggleAll = useCallback( | const handleToggleAll = useCallback( | ||||
@@ -59,6 +62,23 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||||
[allItems, selectedItems.length], | [allItems, selectedItems.length], | ||||
); | ); | ||||
useEffect(() => { | |||||
onChange(selectedItems.map((item) => sortMap[item])); | |||||
}, [onChange, selectedItems, sortMap]); | |||||
const { t } = useTranslation(); | |||||
const groups: LabelGroup[] = uniqBy( | |||||
[ | |||||
...allItems.reduce<LabelGroup[]>((acc, item) => { | |||||
return item.group ? [...acc, item.group] : acc; | |||||
}, []), | |||||
// Items with no group | |||||
{ id: 0, name: t("Ungrouped") }, | |||||
], | |||||
"id", | |||||
); | |||||
const groupedItems = groupBy(allItems, (item) => item.group?.id ?? 0); | |||||
return ( | return ( | ||||
<Box> | <Box> | ||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
@@ -121,18 +141,28 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||||
</Stack> | </Stack> | ||||
<Divider /> | <Divider /> | ||||
</ListSubheader> | </ListSubheader> | ||||
{allItems.map((item) => { | |||||
return ( | |||||
<MenuItem key={item.id} value={item.id} disableRipple> | |||||
<Checkbox | |||||
checked={selectedItems.includes(item.id)} | |||||
disableRipple | |||||
/> | |||||
<ListItemText sx={{ whiteSpace: "normal" }}> | |||||
{item.label} | |||||
</ListItemText> | |||||
</MenuItem> | |||||
); | |||||
{groups.flatMap((group) => { | |||||
const groupItems = groupedItems[group.id]; | |||||
if (!groupItems) return null; | |||||
return [ | |||||
<ListSubheader disableSticky key={`${group.id}-${group.name}`}> | |||||
{group.name} | |||||
</ListSubheader>, | |||||
...groupItems.map((item) => { | |||||
return ( | |||||
<MenuItem key={item.id} value={item.id} disableRipple> | |||||
<Checkbox | |||||
checked={selectedItems.includes(item.id)} | |||||
disableRipple | |||||
/> | |||||
<ListItemText sx={{ whiteSpace: "normal" }}> | |||||
{item.label} | |||||
</ListItemText> | |||||
</MenuItem> | |||||
); | |||||
}), | |||||
]; | |||||
})} | })} | ||||
</Select> | </Select> | ||||
</FormControl> | </FormControl> | ||||
@@ -16,16 +16,25 @@ import Stack from "@mui/material/Stack"; | |||||
import Paper from "@mui/material/Paper"; | import Paper from "@mui/material/Paper"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import ListSubheader from "@mui/material/ListSubheader"; | import ListSubheader from "@mui/material/ListSubheader"; | ||||
import groupBy from "lodash/groupBy"; | |||||
import uniqBy from "lodash/uniqBy"; | |||||
import { useTranslation } from "react-i18next"; | |||||
export interface LabelGroup { | |||||
id: number; | |||||
name: string; | |||||
} | |||||
export interface LabelWithId { | export interface LabelWithId { | ||||
id: string; | |||||
id: number; | |||||
label: string; | label: string; | ||||
group?: LabelGroup; | |||||
} | } | ||||
export interface TransferListProps { | export interface TransferListProps { | ||||
allItems: LabelWithId[]; | allItems: LabelWithId[]; | ||||
initiallySelectedItems: LabelWithId[]; | initiallySelectedItems: LabelWithId[]; | ||||
onChange: () => void; | |||||
onChange: (selectedItems: LabelWithId[]) => void; | |||||
allItemsLabel: string; | allItemsLabel: string; | ||||
selectedItemsLabel: string; | selectedItemsLabel: string; | ||||
} | } | ||||
@@ -48,6 +57,19 @@ const ItemList: React.FC<ItemListProps> = ({ | |||||
handleToggle, | handleToggle, | ||||
handleToggleAll, | handleToggleAll, | ||||
}) => { | }) => { | ||||
const { t } = useTranslation(); | |||||
const groups: LabelGroup[] = uniqBy( | |||||
[ | |||||
...items.reduce<LabelGroup[]>((acc, item) => { | |||||
return item.group ? [...acc, item.group] : acc; | |||||
}, []), | |||||
// Items with no group | |||||
{ id: 0, name: t("Ungrouped") }, | |||||
], | |||||
"id", | |||||
); | |||||
const groupedItems = groupBy(items, (item) => item.group?.id ?? 0); | |||||
return ( | return ( | ||||
<Paper sx={{ width: "100%" }} variant="outlined"> | <Paper sx={{ width: "100%" }} variant="outlined"> | ||||
<List | <List | ||||
@@ -87,14 +109,32 @@ const ItemList: React.FC<ItemListProps> = ({ | |||||
</ListSubheader> | </ListSubheader> | ||||
} | } | ||||
> | > | ||||
{items.map((item) => { | |||||
{groups.map((group) => { | |||||
const groupItems = groupedItems[group.id]; | |||||
if (!groupItems) return null; | |||||
return ( | return ( | ||||
<ListItem key={item.id} onClick={handleToggle(item)}> | |||||
<ListItemIcon> | |||||
<Checkbox checked={checkedItems.includes(item)} tabIndex={-1} /> | |||||
</ListItemIcon> | |||||
<ListItemText primary={item.label} /> | |||||
</ListItem> | |||||
<React.Fragment key={group.id}> | |||||
<ListSubheader | |||||
disableSticky | |||||
sx={{ paddingBlock: 2, lineHeight: 1.8 }} | |||||
> | |||||
{group.name} | |||||
</ListSubheader> | |||||
{groupItems.map((item) => { | |||||
return ( | |||||
<ListItem key={item.id} onClick={handleToggle(item)}> | |||||
<ListItemIcon> | |||||
<Checkbox | |||||
checked={checkedItems.includes(item)} | |||||
tabIndex={-1} | |||||
/> | |||||
</ListItemIcon> | |||||
<ListItemText primary={item.label} /> | |||||
</ListItem> | |||||
); | |||||
})} | |||||
</React.Fragment> | |||||
); | ); | ||||
})} | })} | ||||
</List> | </List> | ||||
@@ -167,6 +207,10 @@ const TransferList: React.FC<TransferListProps> = ({ | |||||
setCheckedList(difference(checkedList, rightListChecked)); | setCheckedList(difference(checkedList, rightListChecked)); | ||||
}; | }; | ||||
React.useEffect(() => { | |||||
onChange(rightList); | |||||
}, [onChange, rightList]); | |||||
return ( | return ( | ||||
<Stack spacing={2} direction="row" alignItems="center" position="relative"> | <Stack spacing={2} direction="row" alignItems="center" position="relative"> | ||||
<ItemList | <ItemList | ||||
@@ -1,2 +1,2 @@ | |||||
export const BASE_API_URL = `${process.env.API_PROTOCOL}://${process.env.API_HOST}:${process.env.API_PORT}`; | |||||
export const LOGIN_API_PATH = `${BASE_API_URL}/api/login`; | |||||
export const BASE_API_URL = `${process.env.API_PROTOCOL}://${process.env.API_HOST}:${process.env.API_PORT}/api`; | |||||
export const LOGIN_API_PATH = `${BASE_API_URL}/login`; |
@@ -2,7 +2,7 @@ import { AuthOptions, Session } from "next-auth"; | |||||
import CredentialsProvider from "next-auth/providers/credentials"; | import CredentialsProvider from "next-auth/providers/credentials"; | ||||
import { LOGIN_API_PATH } from "./api"; | import { LOGIN_API_PATH } from "./api"; | ||||
interface SessionWithTokens extends Session { | |||||
export interface SessionWithTokens extends Session { | |||||
accessToken?: string; | accessToken?: string; | ||||
refreshToken?: string; | refreshToken?: string; | ||||
} | } | ||||
@@ -2,7 +2,7 @@ import { NextRequestWithAuth, withAuth } from "next-auth/middleware"; | |||||
import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
import { NextFetchEvent, NextResponse } from "next/server"; | import { NextFetchEvent, NextResponse } from "next/server"; | ||||
const PUBLIC_ROUTES = ["/login"]; | |||||
const PUBLIC_ROUTES = ["/login", "/logout"]; | |||||
const LANG_QUERY_PARAM = "lang"; | const LANG_QUERY_PARAM = "lang"; | ||||
const authMiddleware = withAuth({ | const authMiddleware = withAuth({ | ||||
@@ -189,7 +189,7 @@ const components: ThemeOptions["components"] = { | |||||
}, | }, | ||||
[`&.Mui-focused`]: { | [`&.Mui-focused`]: { | ||||
backgroundColor: "transparent", | backgroundColor: "transparent", | ||||
borderColor: "palette.primary.main", | |||||
borderColor: palette.primary.main, | |||||
boxShadow: `${palette.primary.main} 0 0 0 2px`, | boxShadow: `${palette.primary.main} 0 0 0 2px`, | ||||
}, | }, | ||||
[`&.Mui-error`]: { | [`&.Mui-error`]: { | ||||
@@ -216,7 +216,7 @@ const components: ThemeOptions["components"] = { | |||||
[`&.Mui-focused`]: { | [`&.Mui-focused`]: { | ||||
backgroundColor: "transparent", | backgroundColor: "transparent", | ||||
[`& .MuiOutlinedInput-notchedOutline`]: { | [`& .MuiOutlinedInput-notchedOutline`]: { | ||||
borderColor: "palette.primary.main", | |||||
borderColor: palette.primary.main, | |||||
boxShadow: `${palette.primary.main} 0 0 0 2px`, | boxShadow: `${palette.primary.main} 0 0 0 2px`, | ||||
}, | }, | ||||
}, | }, | ||||