e70ae79d-5898-4c55-8ceb-d2c701ce08b4

This commit is contained in:
Z User committed 2026-10-26 14:18:39 +00:00
1 parent d226dabba3
commit 5855f699f3
53 files changed
+1911 -44

No files matched your search

+105 -3
View File
@@ -21,6 +21,10 @@ 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'
@@ -72,7 +76,7 @@ const TASK_ICON = {
}
export function ProjectsView() {
const { data, loading } = useFetch<ProjectsData>('/api/aos/projects')
const { data, loading, refetch } = useFetch<ProjectsData>('/api/aos/projects')
const [selectedId, setSelectedId] = useState<string | null>(null)
if (loading || !data) {
@@ -179,7 +183,7 @@ export function ProjectsView() {
{/* Detail */}
<div className="lg:col-span-3">
{selected ? <ProjectDetail project={selected} /> : (
{selected ? <ProjectDetail project={selected} onUpdated={refetch} /> : (
<Card className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground">Wähle ein Projekt.</p>
</Card>
@@ -190,13 +194,62 @@ export function ProjectsView() {
)
}
function ProjectDetail({ project }: { project: Project }) {
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 (
<Card>
<CardHeader className="border-b border-border pb-3">
@@ -220,6 +273,28 @@ function ProjectDetail({ project }: { project: Project }) {
</TabsList>
<TabsContent value="tasks" className="mt-4 space-y-4">
{/* Add task inline */}
<div className="flex items-center gap-2 rounded-md border border-border bg-muted/20 p-2">
<Input
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Neue Aufgabe…"
className="h-8 flex-1 text-xs"
onKeyDown={(e) => e.key === 'Enter' && addTask()}
/>
<Select value={taskPriority} onValueChange={setTaskPriority}>
<SelectTrigger className="h-8 w-28 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="low">Niedrig</SelectItem>
<SelectItem value="medium">Mittel</SelectItem>
<SelectItem value="high">Hoch</SelectItem>
</SelectContent>
</Select>
<Button size="sm" className="h-8 gap-1.5" onClick={addTask} disabled={savingTask || !newTask.trim()}>
{savingTask ? <Loader2 className="size-3.5 animate-spin" /> : <Plus className="size-3.5" />}
Hinzufügen
</Button>
</div>
<TaskColumn label="Offen" tasks={todo} />
<TaskColumn label="In Arbeit" tasks={inProgress} />
<TaskColumn label="Review" tasks={review} />
@@ -260,6 +335,33 @@ function ProjectDetail({ project }: { project: Project }) {
<p className="py-6 text-center text-xs text-muted-foreground">Keine Zeiteinträge.</p>
)}
</div>
{/* Log time inline */}
<div className="flex items-end gap-2 rounded-md border border-border bg-muted/20 p-2">
<div className="flex-1 space-y-1">
<label className="text-[10px] uppercase tracking-wide text-muted-foreground">Beschreibung</label>
<Input
value={timeDesc}
onChange={(e) => setTimeDesc(e.target.value)}
placeholder="z.B. SSO-Integration"
className="h-8 text-xs"
/>
</div>
<div className="w-24 space-y-1">
<label className="text-[10px] uppercase tracking-wide text-muted-foreground">Minuten</label>
<Input
value={timeMin}
onChange={(e) => setTimeMin(e.target.value)}
placeholder="90"
inputMode="numeric"
className="h-8 text-xs"
/>
</div>
<Button size="sm" className="h-8 gap-1.5" onClick={logTime} disabled={savingTime || !timeDesc.trim() || !timeMin}>
{savingTime ? <Loader2 className="size-3.5 animate-spin" /> : <Plus className="size-3.5" />}
Buchen
</Button>
</div>
</TabsContent>
<TabsContent value="budget" className="mt-4 space-y-3">