@@ -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"; | |||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | |||
import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||
import CreateProject from "@/components/CreateProject"; | |||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { isArray } from "lodash"; | |||
import { Metadata } from "next"; | |||
import { notFound } from "next/navigation"; | |||
interface Props { | |||
params: { | |||
projectId: string; | |||
}; | |||
searchParams: { [key: string]: string | string[] | undefined }; | |||
} | |||
export const metadata: Metadata = { | |||
title: "Edit Project", | |||
}; | |||
const Projects: React.FC<Props> = async ({ params }) => { | |||
const Projects: React.FC<Props> = async ({ searchParams }) => { | |||
const { t } = await getServerI18n("projects"); | |||
// Assume projectId is string here | |||
const projectId = searchParams["id"]; | |||
if (!projectId || isArray(projectId)) { | |||
notFound(); | |||
} | |||
// Preload necessary dependencies | |||
fetchAllTasks(); | |||
@@ -46,14 +53,19 @@ const Projects: React.FC<Props> = async ({ params }) => { | |||
preloadTeamLeads(); | |||
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 ( | |||
<> | |||
<Typography variant="h4">{t("Edit Project")}</Typography> | |||
<I18nProvider namespaces={["projects"]}> | |||
<CreateProject isEditMode projectId={params.projectId} /> | |||
<CreateProject isEditMode projectId={projectId} /> | |||
</I18nProvider> | |||
</> | |||
); |
@@ -3,6 +3,16 @@ import { getServerSession } from "next-auth"; | |||
import { headers } from "next/headers"; | |||
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) => { | |||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
const session = await getServerSession<any, SessionWithTokens>(authOptions); | |||
@@ -37,7 +47,10 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||
signOutUser(); | |||
default: | |||
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", | |||
"/projects": "Projects", | |||
"/projects/create": "Create Project", | |||
"/projects/edit": "Edit Project", | |||
"/tasks": "Task Template", | |||
"/tasks/create": "Create Task Template", | |||
"/staffReimbursement": "Staff Reimbursement", | |||
@@ -36,6 +36,7 @@ import { StaffResult } from "@/app/api/staff"; | |||
import { Typography } from "@mui/material"; | |||
import { Grade } from "@/app/api/grades"; | |||
import { Customer, Subsidiary } from "@/app/api/customer"; | |||
import { isEmpty } from "lodash"; | |||
export interface Props { | |||
isEditMode: boolean; | |||
@@ -108,13 +109,17 @@ const CreateProject: React.FC<Props> = ({ | |||
async (data) => { | |||
try { | |||
setServerError(""); | |||
await saveProject(data); | |||
if (isEditMode) { | |||
console.log("edit project", data); | |||
} else { | |||
await saveProject(data); | |||
} | |||
router.replace("/projects"); | |||
} catch (e) { | |||
setServerError(t("An error has occurred. Please try again later.")); | |||
} | |||
}, | |||
[router, t], | |||
[router, t, isEditMode], | |||
); | |||
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | |||
@@ -132,14 +137,19 @@ const CreateProject: React.FC<Props> = ({ | |||
); | |||
const formProps = useForm<CreateProjectInputs>({ | |||
defaultValues: defaultInputs ?? { | |||
defaultValues: { | |||
taskGroups: {}, | |||
allocatedStaffIds: [], | |||
milestones: {}, | |||
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( | |||
(project: ProjectResult) => { | |||
router.push(`/projects/edit/${project.id}`); | |||
router.push(`/projects/edit?id=${project.id}`); | |||
}, | |||
[router], | |||
); | |||