@@ -0,0 +1,17 @@ | |||||
import { getServerI18n } from "@/i18n"; | |||||
import { Stack, Typography, Link } from "@mui/material"; | |||||
import NextLink from "next/link"; | |||||
export default async function NotFound() { | |||||
const { t } = await getServerI18n("projects", "common"); | |||||
return ( | |||||
<Stack spacing={2}> | |||||
<Typography variant="h4">{t("Not Found")}</Typography> | |||||
<Typography variant="body1">{t("The project was not found!")}</Typography> | |||||
<Link href="/projects" component={NextLink} variant="body2"> | |||||
{t("Return to all projects")} | |||||
</Link> | |||||
</Stack> | |||||
); | |||||
} |
@@ -12,23 +12,30 @@ import { | |||||
} from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | ||||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | ||||
import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
import CreateProject from "@/components/CreateProject"; | import CreateProject from "@/components/CreateProject"; | ||||
import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { isArray } from "lodash"; | |||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { notFound } from "next/navigation"; | |||||
interface Props { | interface Props { | ||||
params: { | |||||
projectId: string; | |||||
}; | |||||
searchParams: { [key: string]: string | string[] | undefined }; | |||||
} | } | ||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Edit Project", | title: "Edit Project", | ||||
}; | }; | ||||
const Projects: React.FC<Props> = async ({ params }) => { | |||||
const Projects: React.FC<Props> = async ({ searchParams }) => { | |||||
const { t } = await getServerI18n("projects"); | const { t } = await getServerI18n("projects"); | ||||
// Assume projectId is string here | |||||
const projectId = searchParams["id"]; | |||||
if (!projectId || isArray(projectId)) { | |||||
notFound(); | |||||
} | |||||
// Preload necessary dependencies | // Preload necessary dependencies | ||||
fetchAllTasks(); | fetchAllTasks(); | ||||
@@ -46,14 +53,19 @@ const Projects: React.FC<Props> = async ({ params }) => { | |||||
preloadTeamLeads(); | preloadTeamLeads(); | ||||
preloadStaff(); | preloadStaff(); | ||||
// TODO: Handle not found | |||||
const fetchedProject = await fetchProjectDetails(params.projectId); | |||||
try { | |||||
await fetchProjectDetails(projectId); | |||||
} catch (e) { | |||||
if (e instanceof ServerFetchError && e.response?.status === 404) { | |||||
notFound(); | |||||
} | |||||
} | |||||
return ( | return ( | ||||
<> | <> | ||||
<Typography variant="h4">{t("Edit Project")}</Typography> | <Typography variant="h4">{t("Edit Project")}</Typography> | ||||
<I18nProvider namespaces={["projects"]}> | <I18nProvider namespaces={["projects"]}> | ||||
<CreateProject isEditMode projectId={params.projectId} /> | |||||
<CreateProject isEditMode projectId={projectId} /> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); |
@@ -3,6 +3,16 @@ import { getServerSession } from "next-auth"; | |||||
import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||
import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||
export class ServerFetchError extends Error { | |||||
public readonly response: Response | undefined; | |||||
constructor(message?: string, response?: Response) { | |||||
super(message); | |||||
this.response = response; | |||||
Object.setPrototypeOf(this, ServerFetchError.prototype); | |||||
} | |||||
} | |||||
export const serverFetch: typeof fetch = async (input, init) => { | export const serverFetch: typeof fetch = async (input, init) => { | ||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
const session = await getServerSession<any, SessionWithTokens>(authOptions); | const session = await getServerSession<any, SessionWithTokens>(authOptions); | ||||
@@ -37,7 +47,10 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||||
signOutUser(); | signOutUser(); | ||||
default: | default: | ||||
console.error(await response.text()); | console.error(await response.text()); | ||||
throw Error("Something went wrong fetching data in server."); | |||||
throw new ServerFetchError( | |||||
"Something went wrong fetching data in server.", | |||||
response, | |||||
); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -12,6 +12,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/home": "User Workspace", | "/home": "User Workspace", | ||||
"/projects": "Projects", | "/projects": "Projects", | ||||
"/projects/create": "Create Project", | "/projects/create": "Create Project", | ||||
"/projects/edit": "Edit Project", | |||||
"/tasks": "Task Template", | "/tasks": "Task Template", | ||||
"/tasks/create": "Create Task Template", | "/tasks/create": "Create Task Template", | ||||
"/staffReimbursement": "Staff Reimbursement", | "/staffReimbursement": "Staff Reimbursement", | ||||
@@ -36,6 +36,7 @@ import { StaffResult } from "@/app/api/staff"; | |||||
import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
import { Grade } from "@/app/api/grades"; | import { Grade } from "@/app/api/grades"; | ||||
import { Customer, Subsidiary } from "@/app/api/customer"; | import { Customer, Subsidiary } from "@/app/api/customer"; | ||||
import { isEmpty } from "lodash"; | |||||
export interface Props { | export interface Props { | ||||
isEditMode: boolean; | isEditMode: boolean; | ||||
@@ -108,13 +109,17 @@ const CreateProject: React.FC<Props> = ({ | |||||
async (data) => { | async (data) => { | ||||
try { | try { | ||||
setServerError(""); | setServerError(""); | ||||
await saveProject(data); | |||||
if (isEditMode) { | |||||
console.log("edit project", data); | |||||
} else { | |||||
await saveProject(data); | |||||
} | |||||
router.replace("/projects"); | router.replace("/projects"); | ||||
} catch (e) { | } catch (e) { | ||||
setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
} | } | ||||
}, | }, | ||||
[router, t], | |||||
[router, t, isEditMode], | |||||
); | ); | ||||
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | ||||
@@ -132,14 +137,19 @@ const CreateProject: React.FC<Props> = ({ | |||||
); | ); | ||||
const formProps = useForm<CreateProjectInputs>({ | const formProps = useForm<CreateProjectInputs>({ | ||||
defaultValues: defaultInputs ?? { | |||||
defaultValues: { | |||||
taskGroups: {}, | taskGroups: {}, | ||||
allocatedStaffIds: [], | allocatedStaffIds: [], | ||||
milestones: {}, | milestones: {}, | ||||
totalManhour: 0, | totalManhour: 0, | ||||
manhourPercentageByGrade: grades.reduce((acc, grade) => { | |||||
return { ...acc, [grade.id]: 1 / grades.length }; | |||||
}, {}), | |||||
...defaultInputs, | |||||
// manhourPercentageByGrade should have a sensible default | |||||
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | |||||
? grades.reduce((acc, grade) => { | |||||
return { ...acc, [grade.id]: 1 / grades.length }; | |||||
}, {}) | |||||
: defaultInputs.manhourPercentageByGrade, | |||||
}, | }, | ||||
}); | }); | ||||
@@ -55,7 +55,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => { | |||||
const onProjectClick = useCallback( | const onProjectClick = useCallback( | ||||
(project: ProjectResult) => { | (project: ProjectResult) => { | ||||
router.push(`/projects/edit/${project.id}`); | |||||
router.push(`/projects/edit?id=${project.id}`); | |||||
}, | }, | ||||
[router], | [router], | ||||
); | ); | ||||