diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/controller/ProjectController.java b/backend/server/src/main/java/dev/rheinsw/server/project/controller/ProjectController.java new file mode 100644 index 0000000..13763e9 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/controller/ProjectController.java @@ -0,0 +1,63 @@ +package dev.rheinsw.server.project.controller; + +import dev.rheinsw.server.common.controller.AbstractController; +import dev.rheinsw.server.project.model.CreateCustomerProjectDto; +import dev.rheinsw.server.project.model.Project; +import dev.rheinsw.server.project.model.records.ProjectNote; +import dev.rheinsw.server.project.usecase.ProjectUseCaseImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +@RestController +@RequestMapping("/projects") +@RequiredArgsConstructor +public class ProjectController extends AbstractController { + + private final ProjectUseCaseImpl useCase; + + @PostMapping + public ResponseEntity create(@RequestBody CreateCustomerProjectDto request) { + var currentUser = getUserFromCurrentSession(); + + var now = Instant.now(); + var notes = request.notes().stream().map(n -> new ProjectNote(n.text(), currentUser.getId(), currentUser.getId(), now, now)).toList(); + + var result = useCase.createProject( + currentUser, + request.customerId(), + request.name(), + request.description(), + request.status(), + notes + ); + + return ResponseEntity.ok(result); + } + + @GetMapping + public ResponseEntity findProjectById(@PathVariable("id") UUID id) { + var result = useCase.getProjectById(id); + return ResponseEntity.ok(result); + } + + @GetMapping("/customer/{customerId}") + public ResponseEntity> findAllCustomerProjects(@PathVariable("customerId") UUID customerId) { + var result = useCase.getProjectsByCustomerId(customerId); + return ResponseEntity.ok(result); + } + +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/model/CreateCustomerProjectDto.java b/backend/server/src/main/java/dev/rheinsw/server/project/model/CreateCustomerProjectDto.java new file mode 100644 index 0000000..6b6971c --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/model/CreateCustomerProjectDto.java @@ -0,0 +1,26 @@ +package dev.rheinsw.server.project.model; + +import dev.rheinsw.server.project.model.enums.ProjectStatus; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 13.07.25 + */ +public record CreateCustomerProjectDto( + UUID customerId, // Reference to the related customer + String name, // Project name + String description, // Optional project description + ProjectStatus status, // Enum for project status + List notes, // Optional list of project notes + LocalDate startDate // Project start date +) { + + public record ProjectNoteDto( + String text // Note text + ) { + } +} \ No newline at end of file diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/model/Project.java b/backend/server/src/main/java/dev/rheinsw/server/project/model/Project.java new file mode 100644 index 0000000..3b8c599 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/model/Project.java @@ -0,0 +1,51 @@ +package dev.rheinsw.server.project.model; + +import com.vladmihalcea.hibernate.type.json.JsonType; +import dev.rheinsw.server.common.entity.BaseEntity; +import dev.rheinsw.server.project.model.enums.ProjectStatus; +import dev.rheinsw.server.project.model.records.ProjectNote; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.Type; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +@Entity +@Table(name = "project") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Project extends BaseEntity { + + @Id + private UUID id; + + private UUID customerId; + + private String name; + private String description; + + @Enumerated(EnumType.STRING) + private ProjectStatus status; + + @Column(name = "notes", columnDefinition = "jsonb") + @Type(JsonType.class) + private List notes; +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/model/enums/ProjectStatus.java b/backend/server/src/main/java/dev/rheinsw/server/project/model/enums/ProjectStatus.java new file mode 100644 index 0000000..f38868c --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/model/enums/ProjectStatus.java @@ -0,0 +1,13 @@ +package dev.rheinsw.server.project.model.enums; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +public enum ProjectStatus { + PLANNED, + IN_PROGRESS, + COMPLETED, + ON_HOLD, + CANCELLED +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/model/records/ProjectNote.java b/backend/server/src/main/java/dev/rheinsw/server/project/model/records/ProjectNote.java new file mode 100644 index 0000000..de00929 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/model/records/ProjectNote.java @@ -0,0 +1,14 @@ +package dev.rheinsw.server.project.model.records; + +import java.time.Instant; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +public record ProjectNote(String text, + Long createdBy, + Long updatedBy, + Instant createdAt, + Instant updatedAt) { +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/repository/ProjectRepository.java b/backend/server/src/main/java/dev/rheinsw/server/project/repository/ProjectRepository.java new file mode 100644 index 0000000..93750d2 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/repository/ProjectRepository.java @@ -0,0 +1,16 @@ +package dev.rheinsw.server.project.repository; + +import dev.rheinsw.server.customer.model.Customer; +import dev.rheinsw.server.project.model.Project; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +public interface ProjectRepository extends JpaRepository { + List findByCustomerId(UUID customerId); +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/usecase/CreateProjectUseCase.java b/backend/server/src/main/java/dev/rheinsw/server/project/usecase/CreateProjectUseCase.java new file mode 100644 index 0000000..a66463d --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/usecase/CreateProjectUseCase.java @@ -0,0 +1,23 @@ +package dev.rheinsw.server.project.usecase; + +import dev.rheinsw.server.common.entity.User; +import dev.rheinsw.server.project.model.enums.ProjectStatus; +import dev.rheinsw.server.project.model.records.ProjectNote; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 13.07.25 + */ +public interface CreateProjectUseCase { + UUID createProject( + User creator, + UUID customerId, + String name, + String description, + ProjectStatus status, + List notes + ); +} \ No newline at end of file diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/usecase/LoadProjectUseCase.java b/backend/server/src/main/java/dev/rheinsw/server/project/usecase/LoadProjectUseCase.java new file mode 100644 index 0000000..a8cf1b4 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/usecase/LoadProjectUseCase.java @@ -0,0 +1,16 @@ +package dev.rheinsw.server.project.usecase; + +import dev.rheinsw.server.project.model.Project; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +public interface LoadProjectUseCase { + Project getProjectById(UUID id); + + List getProjectsByCustomerId(UUID customerId); +} diff --git a/backend/server/src/main/java/dev/rheinsw/server/project/usecase/ProjectUseCaseImpl.java b/backend/server/src/main/java/dev/rheinsw/server/project/usecase/ProjectUseCaseImpl.java new file mode 100644 index 0000000..602e216 --- /dev/null +++ b/backend/server/src/main/java/dev/rheinsw/server/project/usecase/ProjectUseCaseImpl.java @@ -0,0 +1,61 @@ +package dev.rheinsw.server.project.usecase; + +import dev.rheinsw.server.common.entity.User; +import dev.rheinsw.server.project.model.Project; +import dev.rheinsw.server.project.model.enums.ProjectStatus; +import dev.rheinsw.server.project.model.records.ProjectNote; +import dev.rheinsw.server.project.repository.ProjectRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +/** + * @author Thatsaphorn Atchariyaphap + * @since 12.07.25 + */ +@Service +@RequiredArgsConstructor +public class ProjectUseCaseImpl implements LoadProjectUseCase, CreateProjectUseCase { + + private final ProjectRepository repository; + + @Override + public UUID createProject( + User creator, + UUID customerId, + String name, + String description, + ProjectStatus status, + List notes + ) { + final var now = Instant.now(); + var enrichedNotes = notes.stream() + .map(n -> new ProjectNote(n.text(), creator.getId(), creator.getId(), now, now)) + .toList(); + + Project project = Project.builder() + .id(UUID.randomUUID()) + .customerId(customerId) + .name(name) + .description(description) + .status(status) + .notes(enrichedNotes) + .build(); + + var savedProject = repository.save(project); + return savedProject.getId(); + } + + @Override + public Project getProjectById(UUID id) { + return repository.findById(id).orElse(null); + } + + @Override + public List getProjectsByCustomerId(UUID customerId) { + return repository.findByCustomerId(customerId); + } +} diff --git a/backend/server/src/main/resources/db/migration/V3__init_project_schema.sql b/backend/server/src/main/resources/db/migration/V3__init_project_schema.sql new file mode 100644 index 0000000..9835942 --- /dev/null +++ b/backend/server/src/main/resources/db/migration/V3__init_project_schema.sql @@ -0,0 +1,22 @@ +-- Migration script for Project table and related components +CREATE TABLE project +( + id UUID PRIMARY KEY, + customer_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + status VARCHAR(50) NOT NULL, -- ProjectStatus enum + notes JSONB, -- JSONB for storing ProjectNotes list + start_date VARCHAR(50), + end_date VARCHAR(50), + created_at TIMESTAMP NOT NULL, -- From BaseEntity + updated_at TIMESTAMP, -- From BaseEntity + created_by BIGINT, -- From BaseEntity + updated_by BIGINT, -- From BaseEntity + version BIGINT -- From BaseEntity +); + +-- Adding a CHECK constraint to enforce valid ProjectStatus values +ALTER TABLE project + ADD CONSTRAINT chk_project_status + CHECK (status IN ('PLANNED', 'IN_PROGRESS', 'COMPLETED', 'ON_HOLD', 'CANCELLED')); diff --git a/internal_frontend/app/api/projects/[id]/route.ts b/internal_frontend/app/api/projects/[id]/route.ts new file mode 100644 index 0000000..ca9811b --- /dev/null +++ b/internal_frontend/app/api/projects/[id]/route.ts @@ -0,0 +1,37 @@ +import {NextRequest, NextResponse} from "next/server"; +import {serverCall} from "@/lib/api/serverCall"; +import {projectRoutes} from "@/app/api/projects/projectRoutes"; + +export async function GET(request: NextRequest) { + try { + // Extract project ID from the URL + const segments = request.url.split("/"); + const projectId = segments.pop(); + + if (!projectId) { + return NextResponse.json( + {error: "Project ID is required"}, + {status: 400} + ); + } + + // Perform server call to fetch the project details + const response = await serverCall(projectRoutes.getById(projectId), "GET"); + + if (!response.ok) { + return NextResponse.json( + {error: "Project not found"}, + {status: response.status} + ); + } + + const project = await response.json(); + return NextResponse.json(project); + } catch (error) { + console.error("Error fetching project:", error); + return NextResponse.json( + {error: "Failed to fetch project"}, + {status: 500} + ); + } +} \ No newline at end of file diff --git a/internal_frontend/app/api/projects/customer/[customerId]/route.ts b/internal_frontend/app/api/projects/customer/[customerId]/route.ts new file mode 100644 index 0000000..acafe40 --- /dev/null +++ b/internal_frontend/app/api/projects/customer/[customerId]/route.ts @@ -0,0 +1,16 @@ +import {NextRequest, NextResponse} from "next/server"; +import {serverCall} from "@/lib/api/serverCall"; +import {projectRoutes} from "@/app/api/projects/projectRoutes"; + +export async function GET(request: NextRequest) { + const segments = request.url.split('/'); + const id = segments[segments.indexOf('customer') + 1]; + const response = await serverCall(projectRoutes.getProjectByCustomerId(id), "GET"); + + if (!response.ok) { + return NextResponse.json({error: "Customer not found"}, {status: 404}); + } + + const customer = await response.json(); + return NextResponse.json(customer); +} \ No newline at end of file diff --git a/internal_frontend/app/api/projects/projectRoutes.ts b/internal_frontend/app/api/projects/projectRoutes.ts new file mode 100644 index 0000000..2571226 --- /dev/null +++ b/internal_frontend/app/api/projects/projectRoutes.ts @@ -0,0 +1,6 @@ +export const projectRoutes = { + 'create': '/api/projects', + getById: (id: string) => `/api/projects/${id}`, + getProjectByCustomerId: (customerId: string) => `/api/projects/customer/${customerId}` + } +; diff --git a/internal_frontend/app/api/projects/route.ts b/internal_frontend/app/api/projects/route.ts new file mode 100644 index 0000000..2fe08e7 --- /dev/null +++ b/internal_frontend/app/api/projects/route.ts @@ -0,0 +1,21 @@ +import {NextRequest, NextResponse} from "next/server"; +import {serverCall} from "@/lib/api/serverCall"; +import {projectRoutes} from "@/app/api/projects/projectRoutes"; + +export async function POST(req: NextRequest) { + try { + // Parse the incoming JSON payload + const body = await req.json(); + + // Make a POST request to the backend using serverCall + const response = await serverCall(projectRoutes.create, "POST", body); + + // Parse and return the backend response + const result = await response.json(); + return NextResponse.json(result); + } catch (error) { + // Handle errors gracefully + console.error("Error creating project:", error); + return NextResponse.json({error: "Failed to create project"}, {status: 500}); + } +} \ No newline at end of file diff --git a/internal_frontend/app/apps/page.tsx b/internal_frontend/app/apps/page.tsx deleted file mode 100644 index a332282..0000000 --- a/internal_frontend/app/apps/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
- apps -
- ); -} diff --git a/internal_frontend/app/customers/[id]/page.tsx b/internal_frontend/app/customers/[id]/page.tsx index c5d276b..8e3bd77 100644 --- a/internal_frontend/app/customers/[id]/page.tsx +++ b/internal_frontend/app/customers/[id]/page.tsx @@ -10,6 +10,7 @@ import CustomerDetailContent from "@/components/customers/details/CustomerDetail import CustomerInformationContent from "@/components/customers/details/sub/ContactInformationContent"; import CustomerPhoneNumberContent from "@/components/customers/details/sub/CustomerPhoneNumberContent"; import CustomerNotesContent from "@/components/customers/details/sub/CustomerNotesContent"; +import CustomerProjectsContent from "@/components/customers/details/sub/CustomerProjectsContent"; import {Customer} from "@/services/customers/entities/customer"; export default function CustomerDetailPage() { @@ -51,7 +52,7 @@ export default function CustomerDetailPage() { }) .catch((error) => { if (!isMounted) return; - console.error('Error fetching customer:', error); + console.error("Error fetching customer:", error); setCustomer(null); setError("Fehler beim Laden der Kundendaten"); }) @@ -73,17 +74,17 @@ export default function CustomerDetailPage() { const formatDate = (date: string) => { try { - const formattedDate = new Date(date).toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' + const formattedDate = new Date(date).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", }); - return formattedDate === 'Invalid Date' ? '-' : formattedDate; + return formattedDate === "Invalid Date" ? "-" : formattedDate; } catch (error) { - console.error('Error formatting date:', error); - return '-'; + console.error("Error formatting date:", error); + return "-"; } }; @@ -93,7 +94,7 @@ export default function CustomerDetailPage() { : "Erstellt von - am -", lastActivityInfo: customer ? `Letzte Aktivität: ${customer.updatedBy || "-"} am ${formatDate(customer.updatedAt)}` - : "Letzte Aktivität: - am -" + : "Letzte Aktivität: - am -", }; const renderMetadata = () => { @@ -140,26 +141,29 @@ export default function CustomerDetailPage() { , - , - - ] - : [] + informationSection={ + + } + phoneNumberSection={ + + } + notesSection={ + + } + projectsSection={ + } /> )} @@ -169,4 +173,4 @@ export default function CustomerDetailPage() { ); -} +} \ No newline at end of file diff --git a/internal_frontend/app/projects/[id]/page.tsx b/internal_frontend/app/projects/[id]/page.tsx new file mode 100644 index 0000000..cc17d00 --- /dev/null +++ b/internal_frontend/app/projects/[id]/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import React, {useEffect, useState} from "react"; +import {useParams, useRouter} from "next/navigation"; +import {ChevronLeft} from "lucide-react"; +import {Button} from "@/components/ui/button"; +import {Skeleton} from "@/components/ui/skeleton"; +import {CustomerProject} from "@/services/projects/entities/customer-project"; + +export default function ProjectDetailPage() { + const router = useRouter(); + const {id} = useParams<{ id: string }>(); + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) { + setError("No project ID provided"); + setLoading(false); + return; + } + + let isMounted = true; + setLoading(true); + setError(null); + + fetch(`/api/projects/${id}`) + .then(async (response) => { + if (!isMounted) return; + + if (response.status === 404) { + setError("Project not found"); + setProject(null); + return; + } + + if (!response.ok) { + throw new Error("Failed to fetch project data"); + } + + const result = await response.json(); + setProject(result); + }) + .catch((err) => { + if (!isMounted) return; + console.error("Error fetching project:", err); + setProject(null); + setError("Error loading project data"); + }) + .finally(() => { + if (isMounted) { + setLoading(false); + } + }); + + return () => { + isMounted = false; + }; + }, [id]); + + if (loading) { + return ( +
+ + + +
+ ); + } + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + return ( +
+
+ +
+
+

{project?.name}

+

{project?.description}

+
+
+ ); +} \ No newline at end of file diff --git a/internal_frontend/app/projects/page.tsx b/internal_frontend/app/projects/page.tsx new file mode 100644 index 0000000..009b163 --- /dev/null +++ b/internal_frontend/app/projects/page.tsx @@ -0,0 +1,7 @@ +export default function ProjectsPage() { + return ( +
+

Project Overview

+
+ ); +} \ No newline at end of file diff --git a/internal_frontend/components/customers/details/CustomerDetailContent.tsx b/internal_frontend/components/customers/details/CustomerDetailContent.tsx index 69bbca0..f805307 100644 --- a/internal_frontend/components/customers/details/CustomerDetailContent.tsx +++ b/internal_frontend/components/customers/details/CustomerDetailContent.tsx @@ -6,10 +6,20 @@ import React from "react"; type Props = { loading: boolean; customer: Customer | null; - sections: React.ReactNode[]; + informationSection: React.ReactNode; + phoneNumberSection: React.ReactNode; + notesSection: React.ReactNode; + projectsSection: React.ReactNode; }; -export default function CustomerDetailContent({loading, customer, sections}: Readonly) { +export default function CustomerDetailContent({ + loading, + customer, + informationSection, + phoneNumberSection, + notesSection, + projectsSection, + }: Readonly) { if (loading) { return (
@@ -40,8 +50,22 @@ export default function CustomerDetailContent({loading, customer, sections}: Rea } return ( -
- {sections} +
+ {/* Row 1: 50/50 layout */} +
+ {informationSection} + {phoneNumberSection} +
+ + {/* Row 2: Fill remaining height */} +
+
+
{notesSection}
+
+
+
{projectsSection}
+
+
); } diff --git a/internal_frontend/components/customers/details/sub/CustomerProjectsContent.tsx b/internal_frontend/components/customers/details/sub/CustomerProjectsContent.tsx new file mode 100644 index 0000000..0476214 --- /dev/null +++ b/internal_frontend/components/customers/details/sub/CustomerProjectsContent.tsx @@ -0,0 +1,271 @@ +import React, {useEffect, useState} from "react"; +import {useRouter} from "next/navigation"; +import {Card} from "@/components/ui/card"; +import {Button} from "@/components/ui/button"; +import {ChevronDown, ChevronRight, ExternalLink, Folder, Pencil, Plus, Trash2} from "lucide-react"; +import {DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,} from "@/components/ui/dialog"; +import {Label} from "@/components/ui/label"; +import {Input} from "@/components/ui/input"; +import {Textarea} from "@/components/ui/textarea"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; +import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip"; +import {AnimatePresence, motion} from "framer-motion"; +import {CustomerProject, CustomerProjectStatus, getStatusText} from "@/services/projects/entities/customer-project"; +import {CreateCustomerProjectDto, ProjectNoteDto} from "@/services/projects/dtos/create-project.dto"; + +interface Props { + customer: { id: string; companyName: string }; + handleOpenDialog: (content: React.ReactNode) => void; +} + +export default function CustomerProjectsContent({customer, handleOpenDialog}: Readonly) { + const router = useRouter(); + const [expandedProjects, setExpandedProjects] = useState([]); + const [projects, setProjects] = useState([]); + const [selectedStatus, setSelectedStatus] = useState(CustomerProjectStatus.PLANNED); + const [notes, setNotes] = useState([]); + + const loadProjects = async () => { + try { + const response = await fetch(`/api/projects/customer/${customer.id}`); + if (!response.ok) throw new Error("Failed to fetch projects"); + const data = await response.json(); + setProjects(data); + } catch (error) { + console.error("Error loading projects:", error); + setProjects([]); + } + }; + + useEffect(() => { + loadProjects(); + }, [customer.id]); + + const toggleProject = (index: number) => { + setExpandedProjects((prev) => + prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index] + ); + }; + + const handleAddNote = () => { + // Ensure a blank note is added when the button is clicked + setNotes((prev) => [...prev, {text: ""}]); + }; + + const handleUpdateNote = (index: number, text: string) => { + setNotes((prev) => + prev.map((note, i) => (i === index ? {...note, text} : note)) + ); + }; + + const handleRemoveNote = (index: number) => { + setNotes((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleAddProject = () => { + handleOpenDialog( + + + Neues Projekt hinzufügen + + Füge ein neues Projekt für {customer.companyName} hinzu. + + +
+
+ + +
+
+ +