Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ logs/
# =========================
coverage/

# Claude Code local settings
.claude/settings.local.json

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneRequest;
import pl.edu.agh.project_manager.controller.dto.project_risk.ProjectRiskRequest;
Expand All @@ -27,10 +26,8 @@ public record ProjectCreationRequest(

UUID projectGroupId,

@NotEmpty(message = "Lista sponsorów nie może być pusta")
List<UUID> sponsors,

@NotEmpty(message = "Lista członków komitetu sterującego nie może być pusta")
List<UUID> committee,

@Valid
Expand All @@ -41,6 +38,8 @@ public record ProjectCreationRequest(
) {
public ProjectCreationRequest {
if (risks == null || risks.isEmpty()) risks = List.of();
if (sponsors == null) sponsors = List.of();
if (committee == null) committee = List.of();
}

public ProjectCreationCommand toCommand(UUID creatorId) {
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/features/project/components/CreateProjectForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useForm, FormProvider } from "react-hook-form";
import { useForm } from "react-hook-form";
import { CreateProjectView } from "./CreateProjectForm.view.tsx";
import type { ProjectCreationRequest } from "../project.types.ts";
import { useCreateProject } from "../project.hooks.ts";
Expand All @@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom";
import { zodResolver } from "@hookform/resolvers/zod";
import { CreateProjectFormSchema } from "../project.schema.ts";
import { getNextDateFromToday } from "../project.utils.ts";
import { Form } from "@/components/ui/form";

export const CreateProjectForm = () => {
const methods = useForm<ProjectCreationRequest>({
Expand All @@ -29,8 +30,7 @@ export const CreateProjectForm = () => {
},
});

const { data: groupsData = [] } = useProjectGroups();
const groups = groupsData.map(g => ({ id: g.id, name: g.name }));
const { data: groups = [] } = useProjectGroups();

const [sponsorsQuery, setSponsorsQuery] = useState("");
const [sponsorsQueryValue] = useDebounce(sponsorsQuery, 300);
Expand All @@ -54,7 +54,7 @@ export const CreateProjectForm = () => {
});

return (
<FormProvider {...methods}>
<Form {...methods}>
<CreateProjectView
onSubmitProject={onSubmit}
isPending={mutation.isPending}
Expand All @@ -64,7 +64,7 @@ export const CreateProjectForm = () => {
onSponsorSearch={setSponsorsQuery}
onCommitteeSearch={setCommitteeQuery}
/>
</FormProvider>
</Form>
);
};

Expand Down
210 changes: 102 additions & 108 deletions frontend/src/features/project/components/CreateProjectForm.view.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { useFieldArray, useFormContext } from "react-hook-form";
import type { ProjectCreationRequest } from "../project.types.ts";
import { useState, useCallback } from "react";
import type { SimpleUserResponse } from "@/features/user-management";
import type { GroupBasicResponse } from "@/features/project_group/project_group.types.ts";
import { UserAutocomplete } from "./UserAutocomplete.tsx";
import { StepBasicInformation } from "./StepBasicInformation.view.tsx";
import { StepMilestonesAndRisks } from "./StepMilestoneAndRisk.tsx";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { getNextDateFromToday } from "../project.utils.ts";

interface CreateProjectViewProps {
onSubmitProject: () => Promise<void>;
isPending: boolean;
groups: { id: string; name: string }[];
groups: GroupBasicResponse[];
foundSponsors: SimpleUserResponse[];
foundCommittee: SimpleUserResponse[];
onSponsorSearch: (query: string) => void;
onCommitteeSearch: (query: string) => void;
message?: string;
}

export const CreateProjectView = ({
Expand All @@ -26,139 +30,129 @@ export const CreateProjectView = ({
foundCommittee,
onSponsorSearch,
onCommitteeSearch,
message
}: CreateProjectViewProps) => {
const {
register,
control,
formState: { errors, isValid },
clearErrors,
watch,
setValue,
getValues,
} = useFormContext<ProjectCreationRequest>();

const milestones = watch("milestones");
const startDate = watch("startDate");
const endDate = watch("endDate");
const today = getNextDateFromToday(0);

const {
fields: riskFields,
append: appendRisk,
remove: removeRisk,
} = useFieldArray({
control,
name: "risks",
});
} = useFieldArray({ control, name: "risks" });

const {
fields: milestonesFields,
append: appendMilestone,
remove: removeMilestone,
} = useFieldArray({
control,
name: "milestones",
});

const [usersById, setUsersById] = useState<Record<string, SimpleUserResponse>>({});

const rememberUser = useCallback((user: SimpleUserResponse) => {
setUsersById((prev) => ({ ...prev, [user.id]: user }));
}, []);
} = useFieldArray({ control, name: "milestones" });

return (
<div className="max-w-3xl mx-auto p-6 bg-white rounded-lg shadow-md mt-10">
<h2 className="text-2xl font-bold mb-6 text-gray-800 text-center">Utwórz Nowy Projekt</h2>

{message && (
<div className={`p-4 mb-4 rounded ${message.includes("Błąd") ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700"}`}>
{message}
</div>
)}

<form className="space-y-8 text-left" onSubmit={(e) => { e.preventDefault(); onSubmitProject(); }}>

<div className="space-y-6">
<StepBasicInformation register={register} errors={errors} groups={groups} />
<div className="max-w-3xl mx-auto p-6 mt-10">
<Card>
<CardHeader>
<CardTitle className="text-2xl text-center">Utwórz Nowy Projekt</CardTitle>
</CardHeader>
<CardContent>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
onSubmitProject();
}}
>
<StepBasicInformation register={register} errors={errors} groups={groups} />

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={control}
name="startDate"
render={({ field }) => (
<FormItem>
<FormLabel>Data rozpoczęcia *</FormLabel>
<FormControl>
<Input type="date" min={today} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Data rozpoczęcia</label>
<input
type="date"
{...register("startDate")}
className={`w-full border rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none transition-colors ${errors.startDate ? "border-red-500" : "border-gray-300"}`}
<FormField
control={control}
name="endDate"
render={({ field }) => (
<FormItem>
<FormLabel>Data zakończenia *</FormLabel>
<FormControl>
<Input type="date" min={startDate || today} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{errors.startDate?.message && <p className="text-red-500 text-sm mt-1">{errors.startDate.message as string}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Data zakończenia</label>
<input
type="date"
{...register("endDate")}
className={`w-full border rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none transition-colors ${errors.endDate ? "border-red-500" : "border-gray-300"}`}

<Separator />

<div className="space-y-4">
<h3 className="text-base font-semibold">Uczestnicy Główni</h3>
<UserAutocomplete
label="Sponsorzy"
foundUsers={foundSponsors}
onSearch={onSponsorSearch}
control={control}
setValue={setValue}
roles="sponsors"
/>
<UserAutocomplete
label="Komitet Sterujący"
foundUsers={foundCommittee}
onSearch={onCommitteeSearch}
control={control}
setValue={setValue}
roles="committee"
/>
{errors.endDate?.message && <p className="text-red-500 text-sm mt-1">{errors.endDate.message as string}</p>}
</div>
</div>
</div>

<div className="space-y-6 pt-6 border-t border-gray-100">
<h3 className="text-lg font-semibold text-gray-800">Uczestnicy Główni</h3>

<div>
<UserAutocomplete
label="Sponsorzy"
foundUsers={foundSponsors}
onSearch={onSponsorSearch}
setValue={setValue}
getValues={getValues}
roles="sponsors"
clearErrors={clearErrors}
usersById={usersById}
onRememberUser={rememberUser}
/>
{errors.sponsors?.message && <p className="text-red-500 text-sm mt-2">{errors.sponsors.message}</p>}
</div>

<div>
<UserAutocomplete
label="Komitet Sterujący"
foundUsers={foundCommittee}
onSearch={onCommitteeSearch}
setValue={setValue}
getValues={getValues}
roles="committee"
clearErrors={clearErrors}
usersById={usersById}
onRememberUser={rememberUser}
/>
{errors.committee?.message && <p className="text-red-500 text-sm mt-2">{errors.committee.message}</p>}
</div>
</div>

<div className="pt-6 border-t border-gray-100">
<StepMilestonesAndRisks
register={register}
errors={errors}
riskFields={riskFields}
appendRisk={appendRisk}
removeRisk={removeRisk}
milestones={milestones}
milestonesFields={milestonesFields}
appendMilestone={appendMilestone}
removeMilestone={removeMilestone}
/>
</div>
<Separator />

<div className="space-y-6">
<StepMilestonesAndRisks
register={register}
control={control}
errors={errors}
riskFields={riskFields}
appendRisk={appendRisk}
removeRisk={removeRisk}
milestonesFields={milestonesFields}
appendMilestone={appendMilestone}
removeMilestone={removeMilestone}
projectStartDate={startDate}
projectEndDate={endDate}
/>
</div>

<div className="pt-6 border-t border-gray-100 flex justify-end">
<Button
type="submit"
disabled={isPending || !isValid}
className="w-full sm:w-auto px-8 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
>
{isPending ? "Tworzenie..." : "Zapisz i utwórz projekt"}
</Button>
</div>
</form>
<div className="flex justify-end pt-2">
<Button
type="submit"
disabled={isPending || !isValid}
className="w-full sm:w-auto px-8"
>
{isPending ? "Tworzenie..." : "Zapisz i utwórz projekt"}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
};
Loading
Loading