272 lines
13 KiB
TypeScript
272 lines
13 KiB
TypeScript
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<Props>) {
|
|
const router = useRouter();
|
|
const [expandedProjects, setExpandedProjects] = useState<number[]>([]);
|
|
const [projects, setProjects] = useState<CustomerProject[]>([]);
|
|
const [selectedStatus, setSelectedStatus] = useState<CustomerProjectStatus>(CustomerProjectStatus.PLANNED);
|
|
const [notes, setNotes] = useState<ProjectNoteDto[]>([]);
|
|
|
|
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(
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Neues Projekt hinzufügen</DialogTitle>
|
|
<DialogDescription>
|
|
Füge ein neues Projekt für <b>{customer.companyName}</b> hinzu.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="project-name">Projektname</Label>
|
|
<Input id="project-name" placeholder="Projektname"/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="project-description">Beschreibung</Label>
|
|
<Textarea
|
|
id="project-description"
|
|
placeholder="Projektbeschreibung"
|
|
className="h-20"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Status</Label>
|
|
<Select
|
|
value={selectedStatus}
|
|
onValueChange={(newStatus: CustomerProjectStatus) => setSelectedStatus(newStatus)}
|
|
>a
|
|
<SelectTrigger id="project-status">
|
|
<SelectValue placeholder="Status auswählen"/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.values(CustomerProjectStatus).map((status) => (
|
|
<SelectItem key={status} value={status}>
|
|
{getStatusText(status)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="project-notes">Notizen</Label>
|
|
<div className="space-y-2">
|
|
{notes.map((note, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Textarea
|
|
placeholder={`Notiz ${index + 1}`}
|
|
value={note.text}
|
|
onChange={(e) => handleUpdateNote(index, e.target.value)}
|
|
className="h-20 flex-1"
|
|
/>
|
|
<Button
|
|
variant="destructive"
|
|
size="icon"
|
|
onClick={() => handleRemoveNote(index)}
|
|
>
|
|
<Trash2 className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<Button size="sm" variant="outline" onClick={handleAddNote}>
|
|
Notiz hinzufügen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => handleOpenDialog(null)}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
onClick={async () => {
|
|
const name = (document.getElementById("project-name") as HTMLInputElement)?.value;
|
|
const description = (document.getElementById("project-description") as HTMLTextAreaElement)?.value;
|
|
|
|
const projectPayload: CreateCustomerProjectDto = {
|
|
customerId: customer.id,
|
|
name,
|
|
description,
|
|
status: selectedStatus,
|
|
notes,
|
|
};
|
|
|
|
try {
|
|
const response = await fetch("/api/projects", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(projectPayload),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error("Failed to create project");
|
|
}
|
|
await loadProjects();
|
|
handleOpenDialog(null);
|
|
} catch (error) {
|
|
console.error("Error creating project:", error);
|
|
}
|
|
}}
|
|
>
|
|
Speichern
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card className="p-4 md:col-span-1">
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex items-center gap-2">
|
|
<Folder className="w-4 h-4 text-muted-foreground"/>
|
|
<h2 className="text-lg font-semibold">Projekte</h2>
|
|
</div>
|
|
<Button size="icon" variant="ghost" onClick={handleAddProject}>
|
|
<Plus className="w-4 h-4"/>
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-1 mt-2">
|
|
{projects.length > 0 ? (
|
|
projects.map((project, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={false}
|
|
className="group rounded-md hover:bg-muted/50 transition-colors"
|
|
>
|
|
<div className="px-2 py-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-2 flex-1 text-left"
|
|
onClick={() => toggleProject(index)}
|
|
>
|
|
<span className="text-muted-foreground">
|
|
{expandedProjects.includes(index) ? (
|
|
<ChevronDown className="w-4 h-4"/>
|
|
) : (
|
|
<ChevronRight className="w-4 h-4"/>
|
|
)}
|
|
</span>
|
|
<span className="font-medium">{project.name}</span>
|
|
<span
|
|
className="ml-2 text-xs rounded-full px-2 py-0.5 bg-muted text-muted-foreground"
|
|
>
|
|
{project.status}
|
|
</span>
|
|
</button>
|
|
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
onClick={() => router.push(`/projects/${project.id}`)}
|
|
>
|
|
<ExternalLink className="w-4 h-4"/>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Projekt öffnen</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button size="icon" variant="ghost">
|
|
<Pencil className="w-4 h-4"/>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Bearbeiten</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button size="icon" variant="ghost">
|
|
<Trash2 className="w-4 h-4 text-destructive"/>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Löschen</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
<AnimatePresence>
|
|
{expandedProjects.includes(index) && (
|
|
<motion.div
|
|
initial={{height: 0, opacity: 0}}
|
|
animate={{height: "auto", opacity: 1}}
|
|
exit={{height: 0, opacity: 0}}
|
|
transition={{duration: 0.2}}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="pt-2 pl-6 text-sm space-y-1">
|
|
<div className="text-muted-foreground">{project.description}</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</motion.div>
|
|
))
|
|
) : (
|
|
<div className="text-muted-foreground italic px-2 py-2">
|
|
Keine Projekte vorhanden.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|