hellth-hub/apps/admin/app/(authed)/einstellungen/punkte/page.tsx

170 lines
7.3 KiB
TypeScript
Raw Normal View History

"use client";
import { CheckIcon } from "@heroicons/react/24/outline";
import { useEffect, useMemo, useState } from "react";
import { trpc } from "@/lib/trpc";
type TemplateDraft = {
id: string;
name: string;
category: string;
defaultMinutes: number;
defaultCalories: number;
defaultPoints: string;
};
function numberInput(value: unknown) {
return String(Number(value) || 0);
}
export default function PunkteEinstellungenPage() {
const utils = trpc.useUtils();
const settingsQuery = trpc.health.pointSettings.useQuery();
const [recipeSubmissionPoints, setRecipeSubmissionPoints] = useState("25");
const [clubActivityDefaultPoints, setClubActivityDefaultPoints] = useState("100");
const [templates, setTemplates] = useState<TemplateDraft[]>([]);
const [saved, setSaved] = useState(false);
const updateMutation = trpc.health.updatePointSettings.useMutation({
onSuccess: async () => {
await Promise.all([
utils.health.pointSettings.invalidate(),
utils.health.pointDefaults.invalidate(),
utils.health.listActivityTemplates.invalidate(),
]);
setSaved(true);
window.setTimeout(() => setSaved(false), 1600);
},
});
useEffect(() => {
if (!settingsQuery.data) return;
setRecipeSubmissionPoints(numberInput(settingsQuery.data.recipeSubmissionPoints));
setClubActivityDefaultPoints(numberInput(settingsQuery.data.clubActivityDefaultPoints));
setTemplates(settingsQuery.data.activityTemplates.map((template) => ({
id: template.id,
name: template.name,
category: template.category,
defaultMinutes: template.defaultMinutes,
defaultCalories: template.defaultCalories,
defaultPoints: numberInput(template.defaultPoints),
})));
}, [settingsQuery.data]);
const groupedTemplates = useMemo(() => {
const groups = new Map<string, TemplateDraft[]>();
for (const template of templates) {
const current = groups.get(template.category) ?? [];
current.push(template);
groups.set(template.category, current);
}
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b, "de-DE"));
}, [templates]);
function updateTemplate(id: string, value: string) {
setTemplates((current) => current.map((template) => (
template.id === id ? { ...template, defaultPoints: value } : template
)));
}
function save() {
updateMutation.mutate({
recipeSubmissionPoints: Number(recipeSubmissionPoints) || 0,
clubActivityDefaultPoints: Number(clubActivityDefaultPoints) || 0,
activityTemplates: templates.map((template) => ({
id: template.id,
defaultPoints: Number(template.defaultPoints) || 0,
})),
});
}
return (
<div className="mx-auto max-w-6xl space-y-5 pb-24">
<header className="rounded-[2rem] bg-white p-6 shadow-sm ring-1 ring-gray-100">
<p className="text-sm font-black uppercase text-[#0F766E]">Admin</p>
<h1 className="mt-2 text-4xl font-black tracking-normal text-gray-950">Punktevergabe</h1>
<p className="mt-2 max-w-2xl text-sm font-semibold text-gray-400">
Globale Punktwerte für Hellfireclub, Rezeptvorschläge und Aktivitätskatalog.
</p>
</header>
<section className="grid gap-4 lg:grid-cols-2">
<div className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
<h2 className="text-xl font-black text-gray-950">Systempunkte</h2>
<div className="mt-4 grid gap-3">
<label className="grid gap-2">
<span className="text-sm font-black text-gray-500">Rezeptvorschlag einreichen</span>
<input
type="number"
value={recipeSubmissionPoints}
onChange={(event) => setRecipeSubmissionPoints(event.target.value)}
className="rounded-2xl border border-gray-200 px-4 py-3 text-sm font-semibold"
/>
</label>
<label className="grid gap-2">
<span className="text-sm font-black text-gray-500">Hellfireclub-Aktivität erstellen, Default</span>
<input
type="number"
value={clubActivityDefaultPoints}
onChange={(event) => setClubActivityDefaultPoints(event.target.value)}
className="rounded-2xl border border-gray-200 px-4 py-3 text-sm font-semibold"
/>
</label>
</div>
</div>
<div className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
<h2 className="text-xl font-black text-gray-950">Hinweis</h2>
<p className="mt-4 text-sm font-semibold text-gray-500">
Bestehende Punkte-Logs bleiben unverändert. Änderungen gelten für neue Aktivitäten,
neue Rezeptvorschläge und neue Einträge aus dem Aktivitätskatalog.
</p>
<button
type="button"
onClick={save}
disabled={updateMutation.isPending}
className="mt-5 inline-flex w-full items-center justify-center gap-2 rounded-full bg-gray-950 px-4 py-4 text-sm font-black text-white disabled:opacity-50"
>
<CheckIcon className="h-5 w-5" />
Punkte speichern
</button>
{saved ? <p className="mt-3 rounded-2xl bg-emerald-50 px-4 py-3 text-sm font-bold text-emerald-800">Punkte gespeichert.</p> : null}
{updateMutation.error ? <p className="mt-3 rounded-2xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800">{updateMutation.error.message}</p> : null}
</div>
</section>
<section className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
<h2 className="text-xl font-black text-gray-950">Aktivitätskatalog</h2>
<div className="mt-5 grid gap-5">
{groupedTemplates.map(([category, entries]) => (
<div key={category}>
<h3 className="text-sm font-black uppercase text-gray-400">{category}</h3>
<div className="mt-2 grid gap-2">
{entries.map((template) => (
<div key={template.id} className="grid gap-3 rounded-3xl bg-gray-50 p-4 md:grid-cols-[1fr_120px_120px_130px] md:items-center">
<div>
<p className="text-sm font-black text-gray-950">{template.name}</p>
<p className="text-xs font-bold text-gray-400">{template.defaultMinutes} min · {template.defaultCalories} kcal</p>
</div>
<span className="text-xs font-bold text-gray-400">{template.id}</span>
<span className="text-xs font-bold text-gray-400">{template.category}</span>
<input
type="number"
value={template.defaultPoints}
onChange={(event) => updateTemplate(template.id, event.target.value)}
className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm font-semibold"
/>
</div>
))}
</div>
</div>
))}
{!templates.length && !settingsQuery.isLoading ? (
<p className="rounded-3xl bg-gray-50 p-4 text-sm font-bold text-gray-400">Keine globalen Aktivitätsvorlagen gefunden.</p>
) : null}
</div>
</section>
</div>
);
}