'use client' import { useState } from 'react' import { Plus, FolderKanban, Clock, Calendar, CheckCircle2, Circle, PlayCircle, Eye, Timer, Play, Pause, } from 'lucide-react' import { useFetch } from '@/hooks/use-fetch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Progress } from '@/components/ui/progress' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Loader2 } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '../primitives' import { formatEuro, formatDate, daysUntil, formatMinutes } from '@/lib/format' import { cn } from '@/lib/utils' interface Project { id: string name: string type: string status: string budgetType: string budgetHours: number | null spentHours: number fixedPriceCents: number | null startDate: string | null dueDate: string | null progress: number company: { id: string; name: string } tasks: Task[] timeEntries: TimeEntry[] } interface Task { id: string; title: string; status: string; assignee: string | null; estimateMin: number | null; priority: string } interface TimeEntry { id: string; description: string; minutes: number; billable: boolean; rateCentsPerHour: number | null; date: string } interface ProjectsData { projects: Project[] minutesThisWeek: number } const STATUS_LABELS: Record = { planning: 'Planung', active: 'Aktiv', review: 'Review', done: 'Abgeschlossen', 'on-hold': 'Pausiert', cancelled: 'Abgebrochen', } const TYPE_LABELS: Record = { website: 'Website', shop: 'Shop', app: 'App', retainer: 'Retainer', } const TASK_ICON = { todo: Circle, 'in-progress': PlayCircle, review: Eye, done: CheckCircle2, } export function ProjectsView() { const { data, loading, refetch } = useFetch('/api/aos/projects') const [selectedId, setSelectedId] = useState(null) if (loading || !data) { return (
{Array.from({ length: 3 }).map((_, i) => )}
) } const { projects, minutesThisWeek } = data const selected = projects.find((p) => p.id === selectedId) ?? projects[0] ?? null const totalHours = projects.reduce((s, p) => s + p.spentHours, 0) const totalBudget = projects.filter((p) => p.fixedPriceCents).reduce((s, p) => s + (p.fixedPriceCents ?? 0), 0) return (
Projekt } />

Aktive Projekte

{projects.filter((p) => p.status === 'active').length}

Stunden gesamt

{totalHours}

Diese Woche

{formatMinutes(minutesThisWeek)}

Fixed-Price-Volumen

{formatEuro(totalBudget)}

{/* Project list */}
{projects.map((p) => { const due = p.dueDate ? daysUntil(p.dueDate) : null const overBudget = p.budgetHours && p.spentHours > p.budgetHours return ( setSelectedId(p.id)} >

{p.name}

{p.company.name}

{STATUS_LABELS[p.status] ?? p.status}
{TYPE_LABELS[p.type] ?? p.type} {p.budgetType} {p.fixedPriceCents && ( {formatEuro(p.fixedPriceCents)} )}
Fortschritt {p.progress}%
{p.spentHours}{p.budgetHours ? ` / ${p.budgetHours} h` : ' h'} {overBudget && · über Budget} {due !== null && ( {due < 0 ? `${Math.abs(due)} d überfällig` : `in ${due} d`} )}
) })}
{/* Detail */}
{selected ? : (

Wähle ein Projekt.

)}
) } function ProjectDetail({ project, onUpdated }: { project: Project; onUpdated?: () => void }) { const [running, setRunning] = useState(false) const [newTask, setNewTask] = useState('') const [taskPriority, setTaskPriority] = useState('medium') const [timeDesc, setTimeDesc] = useState('') const [timeMin, setTimeMin] = useState('') const [savingTask, setSavingTask] = useState(false) const [savingTime, setSavingTime] = useState(false) const todo = project.tasks.filter((t) => t.status === 'todo') const inProgress = project.tasks.filter((t) => t.status === 'in-progress') const review = project.tasks.filter((t) => t.status === 'review') const done = project.tasks.filter((t) => t.status === 'done') const addTask = async () => { if (!newTask.trim()) return setSavingTask(true) const t = toast.loading('Aufgabe wird hinzugefügt…') try { const r = await fetch(`/api/aos/projects/${project.id}/tasks`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: newTask.trim(), priority: taskPriority }), }) if (!r.ok) throw new Error('task failed') toast.success('Aufgabe hinzugefügt', { id: t }) setNewTask('') onUpdated?.() } catch { toast.error('Speichern fehlgeschlagen', { id: t }) } finally { setSavingTask(false) } } const logTime = async () => { const minutes = parseInt(timeMin, 10) if (!timeDesc.trim() || !minutes || minutes < 1) return setSavingTime(true) const t = toast.loading('Zeiteintrag wird gespeichert…') try { const r = await fetch(`/api/aos/projects/${project.id}/time`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ description: timeDesc.trim(), minutes, billable: true, rateCentsPerHour: 9500 }), }) if (!r.ok) throw new Error('time failed') toast.success(`${minutes} min gebucht`, { id: t }) setTimeDesc(''); setTimeMin('') onUpdated?.() } catch { toast.error('Buchen fehlgeschlagen', { id: t }) } finally { setSavingTime(false) } } return (
{project.name}

{project.company.name}

{STATUS_LABELS[project.status]}
Aufgaben Zeit Budget {/* Add task inline */}
setNewTask(e.target.value)} placeholder="Neue Aufgabe…" className="h-8 flex-1 text-xs" onKeyDown={(e) => e.key === 'Enter' && addTask()} />
{/* Live timer mock */}
{running ? '00:42:18' : '00:00:00'}
An: {project.name}
{project.timeEntries.map((te) => (

{te.description}

{formatDate(te.date)}

{formatMinutes(te.minutes)}

{te.billable && te.rateCentsPerHour && (

{formatEuro(te.rateCentsPerHour)}/h

)}
))} {project.timeEntries.length === 0 && (

Keine Zeiteinträge.

)}
{/* Log time inline */}
setTimeDesc(e.target.value)} placeholder="z.B. SSO-Integration" className="h-8 text-xs" />
setTimeMin(e.target.value)} placeholder="90" inputMode="numeric" className="h-8 text-xs" />

Typ

{project.budgetType.replace('-', ' & ')}

{project.fixedPriceCents && (

Fixed Price

{formatEuro(project.fixedPriceCents)}

)} {project.budgetHours && (

Budget-Stunden

{project.budgetHours} h

)}

Verbraucht

{project.spentHours} h

{project.budgetHours && (
Budget-Auslastung {Math.round((project.spentHours / project.budgetHours) * 100)}%
{project.spentHours > project.budgetHours && (

⚠ Über Budget — Change Request prüfen.

)}
)}

Change Requests

Scope-Änderung → bepreistes Angebot → Freigabe → Aufgaben hinzugefügt. (v1 Demo)

) } function TaskColumn({ label, tasks, muted }: { label: string; tasks: Task[]; muted?: boolean }) { return (
{label} ({tasks.length})
{tasks.map((t) => { const Icon = TASK_ICON[t.status as keyof typeof TASK_ICON] ?? Circle return (

{t.title}

{t.assignee && {t.assignee}} {t.estimateMin && · ~{formatMinutes(t.estimateMin)}}
{t.priority === 'high' && !muted && ( hoch )}
) })} {tasks.length === 0 && (
—
)}
) }