From 293dfb7bb464b03ac34401db37a3418605eac974 Mon Sep 17 00:00:00 2001 From: Wayne Date: Tue, 30 Apr 2024 23:55:11 +0900 Subject: [PATCH] Add edit project page --- .../(main)/projects/edit/[projectId]/page.tsx | 62 +++++++ src/app/api/projects/actions.ts | 6 +- src/app/api/projects/index.ts | 14 +- .../CreateProject/CreateProject.tsx | 163 ++++++++++-------- .../CreateProject/CreateProjectWrapper.tsx | 17 +- .../ProjectSearch/ProjectSearch.tsx | 11 +- .../TableCellEdit/TableCellEdit.tsx | 8 +- .../TimesheetTable/EntryInputTable.tsx | 10 +- 8 files changed, 209 insertions(+), 82 deletions(-) create mode 100644 src/app/(main)/projects/edit/[projectId]/page.tsx diff --git a/src/app/(main)/projects/edit/[projectId]/page.tsx b/src/app/(main)/projects/edit/[projectId]/page.tsx new file mode 100644 index 0000000..dbad026 --- /dev/null +++ b/src/app/(main)/projects/edit/[projectId]/page.tsx @@ -0,0 +1,62 @@ +import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; +import { + fetchProjectBuildingTypes, + fetchProjectCategories, + fetchProjectContractTypes, + fetchProjectDetails, + fetchProjectFundingTypes, + fetchProjectLocationTypes, + fetchProjectServiceTypes, + fetchProjectWorkNatures, +} from "@/app/api/projects"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import CreateProject from "@/components/CreateProject"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; + +interface Props { + params: { + projectId: string; + }; +} + +export const metadata: Metadata = { + title: "Edit Project", +}; + +const Projects: React.FC = async ({ params }) => { + const { t } = await getServerI18n("projects"); + + // Preload necessary dependencies + fetchAllTasks(); + fetchTaskTemplates(); + fetchProjectCategories(); + fetchProjectContractTypes(); + fetchProjectFundingTypes(); + fetchProjectLocationTypes(); + fetchProjectServiceTypes(); + fetchProjectBuildingTypes(); + fetchProjectWorkNatures(); + fetchAllCustomers(); + fetchAllSubsidiaries(); + fetchGrades(); + preloadTeamLeads(); + preloadStaff(); + + // TODO: Handle not found + const fetchedProject = await fetchProjectDetails(params.projectId); + + return ( + <> + {t("Edit Project")} + + + + + ); +}; + +export default Projects; diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index 232f863..121111a 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -4,6 +4,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { Task, TaskGroup } from "../tasks"; import { Customer } from "../customer"; +import { revalidateTag } from "next/cache"; export interface CreateProjectInputs { // Project details @@ -62,9 +63,12 @@ export interface PaymentInputs { } export const saveProject = async (data: CreateProjectInputs) => { - return serverFetchJson(`${BASE_API_URL}/projects/new`, { + const newProject = await serverFetchJson(`${BASE_API_URL}/projects/new`, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); + + revalidateTag("projects"); + return newProject; }; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 7bfb067..90b0e10 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -3,6 +3,7 @@ import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import "server-only"; import { Task, TaskGroup } from "../tasks"; +import { CreateProjectInputs } from "./actions"; export interface ProjectResult { id: number; @@ -55,8 +56,8 @@ export interface AssignedProject { tasks: Task[]; milestones: { [taskGroupId: TaskGroup["id"]]: { - startDate: string; - endDate: string; + startDate?: string; + endDate?: string; }; }; // Manhour info @@ -145,3 +146,12 @@ export const fetchAssignedProjects = cache(async () => { }, ); }); + +export const fetchProjectDetails = cache(async (projectId: string) => { + return serverFetchJson( + `${BASE_API_URL}/projects/projectDetails/${projectId}`, + { + next: { tags: [`projectDetails_${projectId}`] }, + }, + ); +}); diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 3e1a3f0..175d712 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -22,7 +22,7 @@ import { useForm, } from "react-hook-form"; import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; -import { Error } from "@mui/icons-material"; +import { Delete, Error, PlayArrow } from "@mui/icons-material"; import { BuildingType, ContractType, @@ -38,6 +38,8 @@ import { Grade } from "@/app/api/grades"; import { Customer, Subsidiary } from "@/app/api/customer"; export interface Props { + isEditMode: boolean; + defaultInputs?: CreateProjectInputs; allTasks: Task[]; projectCategories: ProjectCategory[]; taskTemplates: TaskTemplate[]; @@ -69,6 +71,8 @@ const hasErrorsInTab = ( }; const CreateProject: React.FC = ({ + isEditMode, + defaultInputs, allTasks, projectCategories, taskTemplates, @@ -90,7 +94,7 @@ const CreateProject: React.FC = ({ const router = useRouter(); const handleCancel = () => { - router.back(); + router.replace("/projects"); }; const handleTabChange = useCallback>( @@ -128,7 +132,7 @@ const CreateProject: React.FC = ({ ); const formProps = useForm({ - defaultValues: { + defaultValues: defaultInputs ?? { taskGroups: {}, allocatedStaffIds: [], milestones: {}, @@ -142,76 +146,95 @@ const CreateProject: React.FC = ({ const errors = formProps.formState.errors; return ( - - - - - ) : undefined - } - iconPosition="end" - /> - - - - - { - - } - { - - } - { - - } - {} - {serverError && ( - - {serverError} - - )} - - - - - + )} + + + + + ) : undefined + } + iconPosition="end" + /> + + + + + { + + } + { + + } + { + + } + {} + {serverError && ( + + {serverError} + + )} + + + + + + + ); }; diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 3ca2fae..ab9c830 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -4,6 +4,7 @@ import { fetchProjectBuildingTypes, fetchProjectCategories, fetchProjectContractTypes, + fetchProjectDetails, fetchProjectFundingTypes, fetchProjectLocationTypes, fetchProjectServiceTypes, @@ -13,7 +14,15 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; import { fetchGrades } from "@/app/api/grades"; -const CreateProjectWrapper: React.FC = async () => { +type CreateProjectProps = { isEditMode: false }; +interface EditProjectProps { + isEditMode: true; + projectId: string; +} + +type Props = CreateProjectProps | EditProjectProps; + +const CreateProjectWrapper: React.FC = async (props) => { const [ tasks, taskTemplates, @@ -46,8 +55,14 @@ const CreateProjectWrapper: React.FC = async () => { fetchGrades(), ]); + const projectInfo = props.isEditMode + ? await fetchProjectDetails(props.projectId) + : undefined; + return ( >; type SearchParamNames = keyof SearchQuery; const ProjectSearch: React.FC = ({ projects, projectCategories }) => { + const router = useRouter(); const { t } = useTranslation("projects"); const [filteredProjects, setFilteredProjects] = useState(projects); @@ -51,9 +53,12 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { setFilteredProjects(projects); }, [projects]); - const onProjectClick = useCallback((project: ProjectResult) => { - console.log(project); - }, []); + const onProjectClick = useCallback( + (project: ProjectResult) => { + router.push(`/projects/edit/${project.id}`); + }, + [router], + ); const columns = useMemo[]>( () => [ diff --git a/src/components/TableCellEdit/TableCellEdit.tsx b/src/components/TableCellEdit/TableCellEdit.tsx index a96f0b7..0a366fc 100644 --- a/src/components/TableCellEdit/TableCellEdit.tsx +++ b/src/components/TableCellEdit/TableCellEdit.tsx @@ -9,9 +9,9 @@ import { Box, Input, SxProps, TableCell } from "@mui/material"; interface Props { value: T; - onChange: (newValue?: T) => void; + onChange: (newValue: T) => void; renderValue?: (value: T) => string; - convertValue: (inputValue?: string) => T; + convertValue: (inputValue: string) => T; cellSx?: SxProps; inputSx?: SxProps; } @@ -25,7 +25,7 @@ const TableCellEdit = ({ inputSx, }: Props) => { const [editMode, setEditMode] = useState(false); - const [input, setInput] = useState(); + const [input, setInput] = useState(""); const inputRef = useRef(null); const onClick = useCallback(() => { @@ -41,7 +41,7 @@ const TableCellEdit = ({ const onBlur = useCallback(() => { setEditMode(false); onChange(convertValue(input)); - setInput(undefined); + setInput(""); }, [convertValue, input, onChange]); useEffect(() => { diff --git a/src/components/TimesheetTable/EntryInputTable.tsx b/src/components/TimesheetTable/EntryInputTable.tsx index 15dce8d..87eac77 100644 --- a/src/components/TimesheetTable/EntryInputTable.tsx +++ b/src/components/TimesheetTable/EntryInputTable.tsx @@ -37,7 +37,6 @@ type TimeEntryRow = Partial< _error: string; isPlanned: boolean; id: string; - taskGroupId: number; } >; @@ -221,6 +220,9 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => { valueOptions() { return assignedProjects.map((p) => ({ value: p.id, label: p.name })); }, + valueGetter({ value }) { + return value ?? ""; + }, }, { field: "taskGroupId", @@ -228,6 +230,9 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => { width: 200, editable: true, type: "singleSelect", + valueGetter({ value }) { + return value ?? ""; + }, valueOptions(params) { const updatedRow = params.id ? apiRef.current.getRowWithUpdatedValues(params.id, "") @@ -253,6 +258,9 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => { width: 200, editable: true, type: "singleSelect", + valueGetter({ value }) { + return value ?? ""; + }, valueOptions(params) { const updatedRow = params.id ? apiRef.current.getRowWithUpdatedValues(params.id, "")