Initial commit: WrestleDesk full project
- Django backend with DRF (clubs, wrestlers, trainers, exercises, templates, trainings, homework, locations, leistungstest) - Next.js 16 frontend with React, Shadcn UI, Tailwind - JWT authentication - Full CRUD for all entities - Calendar view for trainings - Homework management system - Leistungstest tracking
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useAuth } from "@/lib/auth"
|
||||
import { apiFetch, ITrainingHomeworkAssignment, PaginatedResponse } from "@/lib/api"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||
import { FadeIn } from "@/components/ui/animations"
|
||||
import { Check, X, Loader2, BookOpen, Calendar, Dumbbell } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const groupConfig = {
|
||||
kids: { label: "Kinder", class: "bg-primary/10 text-primary" },
|
||||
youth: { label: "Jugend", class: "bg-secondary/10 text-secondary" },
|
||||
adults: { label: "Erwachsene", class: "bg-accent/10 text-accent" },
|
||||
}
|
||||
|
||||
const categoryConfig = {
|
||||
warmup: { label: "Aufwärmen", class: "bg-primary/10 text-primary" },
|
||||
kraft: { label: "Kraft", class: "bg-destructive/10 text-destructive" },
|
||||
technik: { label: "Technik", class: "bg-secondary/10 text-secondary" },
|
||||
ausdauer: { label: "Ausdauer", class: "bg-accent/10 text-accent" },
|
||||
spiele: { label: "Spiele", class: "bg-muted text-muted-foreground" },
|
||||
cool_down: { label: "Abkühlung", class: "bg-primary/5 text-primary" },
|
||||
}
|
||||
|
||||
interface GroupedByWrestler {
|
||||
wrestlerId: number
|
||||
wrestlerName: string
|
||||
wrestlerGroup: string
|
||||
assignments: ITrainingHomeworkAssignment[]
|
||||
completedCount: number
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
interface WrestlerCentricViewProps {
|
||||
assignments: ITrainingHomeworkAssignment[]
|
||||
onToggleComplete: (id: number, current: boolean) => void
|
||||
togglingId: number | null
|
||||
}
|
||||
|
||||
export function WrestlerCentricView({ assignments, onToggleComplete, togglingId }: WrestlerCentricViewProps) {
|
||||
const [selectedWrestler, setSelectedWrestler] = useState<GroupedByWrestler | null>(null)
|
||||
|
||||
// Group assignments by wrestler
|
||||
const groupedByWrestler: GroupedByWrestler[] = assignments.reduce((acc, assignment) => {
|
||||
const existing = acc.find(g => g.wrestlerId === assignment.wrestler)
|
||||
if (existing) {
|
||||
existing.assignments.push(assignment)
|
||||
if (assignment.is_completed) existing.completedCount++
|
||||
existing.totalCount++
|
||||
} else {
|
||||
acc.push({
|
||||
wrestlerId: assignment.wrestler,
|
||||
wrestlerName: assignment.wrestler_name,
|
||||
wrestlerGroup: assignment.wrestler_group,
|
||||
assignments: [assignment],
|
||||
completedCount: assignment.is_completed ? 1 : 0,
|
||||
totalCount: 1,
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [] as GroupedByWrestler[])
|
||||
|
||||
// Sort wrestlers by: those with incomplete homework first, then by name
|
||||
groupedByWrestler.sort((a, b) => {
|
||||
const aIncomplete = a.totalCount - a.completedCount
|
||||
const bIncomplete = b.totalCount - b.completedCount
|
||||
if (aIncomplete !== bIncomplete) return bIncomplete - aIncomplete
|
||||
return a.wrestlerName.localeCompare(b.wrestlerName)
|
||||
})
|
||||
|
||||
const getProgress = (wrestler: GroupedByWrestler) => {
|
||||
if (wrestler.totalCount === 0) return 0
|
||||
return Math.round((wrestler.completedCount / wrestler.totalCount) * 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{groupedByWrestler.map((wrestler) => {
|
||||
const progress = getProgress(wrestler)
|
||||
const isComplete = progress === 100
|
||||
|
||||
return (
|
||||
<FadeIn key={wrestler.wrestlerId}>
|
||||
<Card
|
||||
className={`cursor-pointer hover:shadow-md transition-all ${isComplete ? 'opacity-75' : ''}`}
|
||||
onClick={() => setSelectedWrestler(wrestler)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar size="default">
|
||||
<AvatarFallback className="bg-primary/10 text-primary">
|
||||
{wrestler.wrestlerName?.split(" ").map(n => n[0]).slice(0,2).join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<CardTitle className="text-base">{wrestler.wrestlerName}</CardTitle>
|
||||
<Badge className={groupConfig[wrestler.wrestlerGroup as keyof typeof groupConfig]?.class}>
|
||||
{groupConfig[wrestler.wrestlerGroup as keyof typeof groupConfig]?.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`text-lg font-bold ${isComplete ? 'text-green-600' : 'text-orange-500'}`}>
|
||||
{wrestler.completedCount}/{wrestler.totalCount}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{isComplete ? 'Alle erledigt' : 'Offen'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3">
|
||||
<Progress value={progress} className="flex-1 h-2" />
|
||||
<span className="text-sm font-medium w-12">{progress}%</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Letztes Training: {wrestler.assignments[0]?.training_date
|
||||
? new Date(wrestler.assignments[0].training_date).toLocaleDateString("de-DE")
|
||||
: "Keine"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FadeIn>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Detail Sheet */}
|
||||
<Sheet open={!!selectedWrestler} onOpenChange={() => setSelectedWrestler(null)}>
|
||||
<SheetContent side="right" className="w-[400px] sm:max-w-[400px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{selectedWrestler?.wrestlerName}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
{selectedWrestler && (
|
||||
<div className="mt-4 space-y-4">
|
||||
{selectedWrestler.assignments
|
||||
.sort((a, b) => new Date(b.training_date).getTime() - new Date(a.training_date).getTime())
|
||||
.map(assignment => (
|
||||
<div key={assignment.id} className="border rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">
|
||||
{new Date(assignment.training_date).toLocaleDateString("de-DE")}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={assignment.is_completed ? "default" : "secondary"}
|
||||
className={assignment.is_completed ? "bg-green-600" : ""}
|
||||
>
|
||||
{assignment.is_completed ? "Erledigt" : "Offen"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleComplete(assignment.id, assignment.is_completed)}
|
||||
disabled={togglingId === assignment.id}
|
||||
>
|
||||
{togglingId === assignment.id ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : assignment.is_completed ? (
|
||||
<X className="w-4 h-4 text-red-500" />
|
||||
) : (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{assignment.exercises.map(ex => (
|
||||
<div key={ex.id} className="flex items-center gap-2 text-sm">
|
||||
<Badge className={categoryConfig[ex.exercise_category as keyof typeof categoryConfig]?.class}>
|
||||
{categoryConfig[ex.exercise_category as keyof typeof categoryConfig]?.label}
|
||||
</Badge>
|
||||
<span>{ex.exercise_name}</span>
|
||||
{ex.reps && <span className="text-muted-foreground">{ex.reps}x</span>}
|
||||
{ex.time_minutes && <span className="text-muted-foreground">{ex.time_minutes}s</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{assignment.notes && (
|
||||
<p className="mt-2 text-xs text-muted-foreground border-t pt-2">
|
||||
{assignment.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user