Bladeren bron

Add edit project page

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 jaar geleden
bovenliggende
commit
293dfb7bb4
8 gewijzigde bestanden met toevoegingen van 209 en 82 verwijderingen
  1. +62
    -0
      src/app/(main)/projects/edit/[projectId]/page.tsx
  2. +5
    -1
      src/app/api/projects/actions.ts
  3. +12
    -2
      src/app/api/projects/index.ts
  4. +93
    -70
      src/components/CreateProject/CreateProject.tsx
  5. +16
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  6. +8
    -3
      src/components/ProjectSearch/ProjectSearch.tsx
  7. +4
    -4
      src/components/TableCellEdit/TableCellEdit.tsx
  8. +9
    -1
      src/components/TimesheetTable/EntryInputTable.tsx

+ 62
- 0
src/app/(main)/projects/edit/[projectId]/page.tsx Bestand weergeven

@@ -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<Props> = 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 (
<>
<Typography variant="h4">{t("Edit Project")}</Typography>
<I18nProvider namespaces={["projects"]}>
<CreateProject isEditMode projectId={params.projectId} />
</I18nProvider>
</>
);
};

export default Projects;

+ 5
- 1
src/app/api/projects/actions.ts Bestand weergeven

@@ -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;
};

+ 12
- 2
src/app/api/projects/index.ts Bestand weergeven

@@ -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<CreateProjectInputs>(
`${BASE_API_URL}/projects/projectDetails/${projectId}`,
{
next: { tags: [`projectDetails_${projectId}`] },
},
);
});

+ 93
- 70
src/components/CreateProject/CreateProject.tsx Bestand weergeven

@@ -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<Props> = ({
isEditMode,
defaultInputs,
allTasks,
projectCategories,
taskTemplates,
@@ -90,7 +94,7 @@ const CreateProject: React.FC<Props> = ({
const router = useRouter();

const handleCancel = () => {
router.back();
router.replace("/projects");
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
@@ -128,7 +132,7 @@ const CreateProject: React.FC<Props> = ({
);

const formProps = useForm<CreateProjectInputs>({
defaultValues: {
defaultValues: defaultInputs ?? {
taskGroups: {},
allocatedStaffIds: [],
milestones: {},
@@ -142,76 +146,95 @@ const CreateProject: React.FC<Props> = ({
const errors = formProps.formState.errors;

return (
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab
label={t("Project and Client Details")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("Project Task Setup")} iconPosition="end" />
<Tab label={t("Staff Allocation and Resource")} iconPosition="end" />
<Tab label={t("Milestone")} iconPosition="end" />
</Tabs>
{
<ProjectClientDetails
buildingTypes={buildingTypes}
workNatures={workNatures}
contractTypes={contractTypes}
fundingTypes={fundingTypes}
locationTypes={locationTypes}
serviceTypes={serviceTypes}
allCustomers={allCustomers}
allSubsidiaries={allSubsidiaries}
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}
/>
}
{
<TaskSetup
allTasks={allTasks}
taskTemplates={taskTemplates}
isActive={tabIndex === 1}
/>
}
{
<StaffAllocation
isActive={tabIndex === 2}
allTasks={allTasks}
grades={grades}
allStaffs={allStaffs}
/>
}
{<Milestone allTasks={allTasks} isActive={tabIndex === 3} />}
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
<>
{isEditMode && (
<Stack direction="row" gap={1}>
<Button variant="contained" startIcon={<PlayArrow />} color="success">
{t("Start Project")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Confirm")}
<Button variant="outlined" startIcon={<Delete />} color="error">
{t("Delete Project")}
</Button>
</Stack>
</Stack>
</FormProvider>
)}
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("Project and Client Details")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("Project Task Setup")} iconPosition="end" />
<Tab
label={t("Staff Allocation and Resource")}
iconPosition="end"
/>
<Tab label={t("Milestone")} iconPosition="end" />
</Tabs>
{
<ProjectClientDetails
buildingTypes={buildingTypes}
workNatures={workNatures}
contractTypes={contractTypes}
fundingTypes={fundingTypes}
locationTypes={locationTypes}
serviceTypes={serviceTypes}
allCustomers={allCustomers}
allSubsidiaries={allSubsidiaries}
projectCategories={projectCategories}
teamLeads={teamLeads}
isActive={tabIndex === 0}
/>
}
{
<TaskSetup
allTasks={allTasks}
taskTemplates={taskTemplates}
isActive={tabIndex === 1}
/>
}
{
<StaffAllocation
isActive={tabIndex === 2}
allTasks={allTasks}
grades={grades}
allStaffs={allStaffs}
/>
}
{<Milestone allTasks={allTasks} isActive={tabIndex === 3} />}
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<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">
{isEditMode ? t("Save") : t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};



+ 16
- 1
src/components/CreateProject/CreateProjectWrapper.tsx Bestand weergeven

@@ -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<Props> = 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 (
<CreateProject
isEditMode={props.isEditMode}
defaultInputs={projectInfo}
allTasks={tasks}
projectCategories={projectCategories}
taskTemplates={taskTemplates}


+ 8
- 3
src/components/ProjectSearch/ProjectSearch.tsx Bestand weergeven

@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import uniq from "lodash/uniq";
import { useRouter } from "next/navigation";

interface Props {
projects: ProjectResult[];
@@ -17,6 +18,7 @@ type SearchQuery = Partial<Omit<ProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
const router = useRouter();
const { t } = useTranslation("projects");

const [filteredProjects, setFilteredProjects] = useState(projects);
@@ -51,9 +53,12 @@ const ProjectSearch: React.FC<Props> = ({ 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<Column<ProjectResult>[]>(
() => [


+ 4
- 4
src/components/TableCellEdit/TableCellEdit.tsx Bestand weergeven

@@ -9,9 +9,9 @@ import { Box, Input, SxProps, TableCell } from "@mui/material";

interface Props<T> {
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 = <T,>({
inputSx,
}: Props<T>) => {
const [editMode, setEditMode] = useState(false);
const [input, setInput] = useState<string>();
const [input, setInput] = useState<string>("");
const inputRef = useRef<HTMLInputElement>(null);

const onClick = useCallback(() => {
@@ -41,7 +41,7 @@ const TableCellEdit = <T,>({
const onBlur = useCallback(() => {
setEditMode(false);
onChange(convertValue(input));
setInput(undefined);
setInput("");
}, [convertValue, input, onChange]);

useEffect(() => {


+ 9
- 1
src/components/TimesheetTable/EntryInputTable.tsx Bestand weergeven

@@ -37,7 +37,6 @@ type TimeEntryRow = Partial<
_error: string;
isPlanned: boolean;
id: string;
taskGroupId: number;
}
>;

@@ -221,6 +220,9 @@ const EntryInputTable: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ day, assignedProjects }) => {
width: 200,
editable: true,
type: "singleSelect",
valueGetter({ value }) {
return value ?? "";
},
valueOptions(params) {
const updatedRow = params.id
? apiRef.current.getRowWithUpdatedValues(params.id, "")


Laden…
Annuleren
Opslaan