feat(archiv): Uebersicht, Volltextsuche und Druckansicht
Der Einstieg am Schreibtisch und das Gedaechtnis der Anwendung. Die Uebersicht
sortiert nach Dringlichkeit — ueberfaellig zuerst, und das ist das Einzige in
Warnfarbe; alles andere auf der Seite ist Auskunft.
Das Archiv sucht ueber Titel, Notizen, erfasste Werte und Kommentare in einer
SQL-Anweisung: `Schrittergebnis.wert` ist ein Json, und nur `("wert")::text`
trifft den Trigramm-Index aus #006. Vor allem aber gehoert die Fundstelle zum
Treffer — ein Treffer ohne sichtbaren Grund ist fuer den Suchenden wertlos.
Die Druckansicht ist ein eigenes Lesemodell und kein Schalter an
`laufZumAbarbeiten`: dort entsteht beim Fuellen der Befehle ein Passwort, und auf
einem Blatt, das jemand abheftet, darf keines stehen. Vom Mitarbeiter holt sie
nur Name, UPN und Abteilung — nicht alles und dann gefiltert.
Der unabhaengige Review fand zwei Blocker, beide im Dashboard: „ueberfaellig“
verglich mit dem Augenblick statt mit dem Tagesbeginn, und der Schnellstart
zaehlte Laeufe fremder Kunden mit. Was „ueberfaellig“ heisst, steht seitdem in
`src/lib/fristen.ts` — eine Regel fuer Lesemodell und Anzeige.
Closes #15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #29.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { Archive } from "lucide-react";
|
||||
import { ArchivFilter, type ArchivFilterfelder } from "@/components/archiv/archiv-filter";
|
||||
import { ArchivTreffer } from "@/components/archiv/archiv-treffer";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { kundenEbeneSichtbar } from "@/server/mandant";
|
||||
import {
|
||||
ARCHIV_HOECHSTENS,
|
||||
SUCHE_MINDESTZEICHEN,
|
||||
archivFilterwerte,
|
||||
archivSuche,
|
||||
datumAusText,
|
||||
type ArchivFilter as ArchivFilterwerte,
|
||||
} from "@/server/queries/archiv";
|
||||
|
||||
/**
|
||||
* Das Archiv (#015).
|
||||
*
|
||||
* Das Gedaechtnis der Anwendung: durchsucht werden abgeschlossene und
|
||||
* archivierte Laeufe, und zwar in `Lauf.titel`, `Schrittergebnis.notiz`,
|
||||
* `Schrittergebnis.wert` und `Kommentar.rumpf`. Was genau dabei geschieht, steht
|
||||
* in `src/server/queries/archiv.ts`; diese Seite liest die Adresszeile, prueft
|
||||
* ihr Recht und stellt dar.
|
||||
*
|
||||
* **Sie prueft selbst.** Die Huelle unter `(app)/layout.tsx` ist das zweite Netz
|
||||
* und nicht das erste: Next rendert Layouts bei Navigation innerhalb desselben
|
||||
* Layouts nicht neu (#007).
|
||||
*/
|
||||
|
||||
export default async function ArchivSeite({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const { benutzer } = await sitzungErforderlich("lesen", "/archiv");
|
||||
const parameter = await searchParams;
|
||||
const einzeln = (name: string): string =>
|
||||
(Array.isArray(parameter[name]) ? parameter[name][0] : parameter[name])?.trim() ?? "";
|
||||
|
||||
const felder: ArchivFilterfelder = {
|
||||
suche: einzeln("suche"),
|
||||
vorlage: einzeln("vorlage"),
|
||||
mitarbeiter: einzeln("mitarbeiter"),
|
||||
abteilung: einzeln("abteilung"),
|
||||
geraet: einzeln("geraet"),
|
||||
bearbeiter: einzeln("bearbeiter"),
|
||||
kunde: einzeln("kunde"),
|
||||
von: einzeln("von"),
|
||||
bis: einzeln("bis"),
|
||||
laufende: einzeln("laufende") === "1",
|
||||
};
|
||||
|
||||
/*
|
||||
* Ein vertippter Parameter filtert nicht, statt eine Fehlerseite zu zeigen –
|
||||
* dieselbe Entscheidung wie bei der Geraeteliste (#011). Eine erfundene
|
||||
* Kennung bleibt dagegen stehen: sie ist kein Formfehler, sondern eine Suche,
|
||||
* die nichts findet. Ueber die Mandantengrenze kommt sie nicht (siehe
|
||||
* archivSuche).
|
||||
*/
|
||||
const von = datumAusText(felder.von);
|
||||
const bis = datumAusText(felder.bis);
|
||||
const filter: ArchivFilterwerte = {
|
||||
...(felder.suche ? { suche: felder.suche } : {}),
|
||||
...(felder.vorlage ? { vorlageId: felder.vorlage } : {}),
|
||||
...(felder.mitarbeiter ? { mitarbeiterId: felder.mitarbeiter } : {}),
|
||||
...(felder.abteilung ? { abteilungId: felder.abteilung } : {}),
|
||||
...(felder.geraet ? { geraetId: felder.geraet } : {}),
|
||||
...(felder.bearbeiter ? { bearbeiterId: felder.bearbeiter } : {}),
|
||||
...(felder.kunde ? { kundeId: felder.kunde } : {}),
|
||||
...(von ? { von } : {}),
|
||||
...(bis ? { bis } : {}),
|
||||
...(felder.laufende ? { auchLaufende: true } : {}),
|
||||
};
|
||||
|
||||
const [ergebnis, werte, kundenEbene] = await Promise.all([
|
||||
archivSuche(benutzer, filter),
|
||||
archivFilterwerte(benutzer),
|
||||
kundenEbeneSichtbar(),
|
||||
]);
|
||||
// Hervorgehoben wird nur, wonach auch gesucht wurde: ein verworfener Begriff
|
||||
// hat keine Fundstellen und darf keine markieren.
|
||||
const begriff = ergebnis.sucheZuKurz ? "" : felder.suche;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
||||
<Archive className="size-6 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||
Archiv
|
||||
</h1>
|
||||
<p className="mt-2 max-w-prose text-nebentext">
|
||||
Durchsucht werden abgeschlossene und archivierte Läufe — ihr Titel, die Notizen und Werte der Schritte
|
||||
und die Kommentare. Ein archivierter Lauf ändert sich nicht mehr; zurückholen kann ihn nur ein
|
||||
Administrator.
|
||||
</p>
|
||||
|
||||
<ArchivFilter felder={felder} werte={werte} kundenEbene={kundenEbene} />
|
||||
|
||||
{ergebnis.sucheZuKurz ? (
|
||||
<p className="mt-6 rounded-karte border border-linie bg-flaeche p-5 text-sm text-nebentext">
|
||||
„{felder.suche}“ ist als Suchbegriff zu kurz — es braucht mindestens {SUCHE_MINDESTZEICHEN} Zeichen.
|
||||
Gezeigt werden deshalb die Treffer der übrigen Filter.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{ergebnis.zeilen.length === 0 ? (
|
||||
<p className="mt-6 rounded-karte border border-linie bg-flaeche p-5 text-sm text-nebentext">
|
||||
Kein Lauf passt dazu. Laufende Läufe stehen erst mit dem Häkchen „auch offene und laufende Läufe“
|
||||
dabei — bis zum Abschluss gehören sie in die Laufliste.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-6 max-w-prose text-sm text-nebentext">
|
||||
{ergebnis.zeilen.length === 1 ? "Ein Lauf gefunden" : `${ergebnis.zeilen.length} Läufe gefunden`}
|
||||
{/* Eine gekappte Liste sagt das – sonst fehlte ein Lauf stillschweigend. */}
|
||||
{ergebnis.gekappt
|
||||
? `. Es sind mehr als ${ARCHIV_HOECHSTENS}; gezeigt werden die zuletzt abgelegten. Ein engerer Zeitraum oder ein genauerer Begriff holt die übrigen hervor.`
|
||||
: "."}
|
||||
</p>
|
||||
<ul className="mt-3 flex flex-col gap-3">
|
||||
{ergebnis.zeilen.map((zeile) => (
|
||||
<ArchivTreffer key={zeile.id} zeile={zeile} begriff={begriff} kundenEbene={kundenEbene} />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowLeft, MessageSquare, Star } from "lucide-react";
|
||||
import { ArrowLeft, MessageSquare, Printer, Star } from "lucide-react";
|
||||
import { LaufStatus, SchrittTyp, SchrittZustand } from "@/generated/prisma/enums";
|
||||
import { Markdown } from "@/components/markdown";
|
||||
import { AbschnittHuelle } from "@/components/laeufe/abschnitt-huelle";
|
||||
@@ -364,6 +364,21 @@ export default async function LaufSeite({
|
||||
>
|
||||
{ausfuehrlich ? "Kompakt" : "Ausführlich"}
|
||||
</Link>
|
||||
|
||||
{/*
|
||||
* Die Druckansicht liegt ausserhalb der Huelle und traegt deshalb kein
|
||||
* `Link`-Vorabladen: sie ist hell, ohne Navigation und fuer Papier
|
||||
* gemacht (#015). Auf dem Blatt steht **kein** Passwort — auch dort
|
||||
* nicht, wo im Lauf eines stuende.
|
||||
*/}
|
||||
<Link
|
||||
href={`/drucken/lauf/${lauf.id}`}
|
||||
prefetch={false}
|
||||
className="flex min-h-11 items-center gap-2 rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
<Printer className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
Drucken
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4">
|
||||
|
||||
+284
-18
@@ -1,27 +1,293 @@
|
||||
export default function Startseite() {
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Clock, ListChecks, Rocket, TriangleAlert } from "lucide-react";
|
||||
import { LaufStatus } from "@/generated/prisma/enums";
|
||||
import { fristVerstrichen } from "@/lib/fristen";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import {
|
||||
UEBERSICHT_JE_BLOCK,
|
||||
haeufigeVorlagen,
|
||||
uebersicht,
|
||||
type HaeufigeVorlage,
|
||||
type UebersichtsLauf,
|
||||
} from "@/server/queries/uebersicht";
|
||||
|
||||
/**
|
||||
* Die Uebersicht (#015).
|
||||
*
|
||||
* Der Einstieg am Schreibtisch — das Gegenstueck zum Aufkleber am Geraet. Sie
|
||||
* zeigt vier Dinge und sonst nichts: was ueberfaellig ist, was bei mir liegt,
|
||||
* woran zuletzt gearbeitet wurde, und was man haeufig anfaengt.
|
||||
*
|
||||
* **Ueberfaellig steht oben und ist das Einzige in Warnfarbe.** Alles andere auf
|
||||
* dieser Seite ist Auskunft; das hier ist eine Aufforderung. Wuerde jeder Block
|
||||
* rufen, riefe keiner.
|
||||
*
|
||||
* Leere Bloecke verschwinden nicht, sie sagen es: eine Seite, deren Abschnitte
|
||||
* je nach Datenlage kommen und gehen, laesst sich nicht lesen.
|
||||
*/
|
||||
|
||||
const DATUM = new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
|
||||
const STATUS_TEXT: Record<LaufStatus, string> = {
|
||||
OFFEN: "offen",
|
||||
IN_ARBEIT: "in Arbeit",
|
||||
ABGESCHLOSSEN: "abgeschlossen",
|
||||
ARCHIVIERT: "archiviert",
|
||||
};
|
||||
|
||||
function Fortschritt({ lauf }: { lauf: UebersichtsLauf }) {
|
||||
const anteil = lauf.anzahlSchritte === 0 ? 0 : lauf.anzahlErledigt / lauf.anzahlSchritte;
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div
|
||||
className="h-1.5 w-24 overflow-hidden rounded-pille bg-flaeche-hoch"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={lauf.anzahlSchritte}
|
||||
aria-valuenow={lauf.anzahlErledigt}
|
||||
aria-label="Fortschritt"
|
||||
>
|
||||
<div className="h-full bg-akzent" style={{ width: `${Math.round(anteil * 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-nebentext">
|
||||
{lauf.anzahlErledigt} von {lauf.anzahlSchritte}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LaufZeile({
|
||||
lauf,
|
||||
zeigeFaellig,
|
||||
jetzt,
|
||||
}: {
|
||||
lauf: UebersichtsLauf;
|
||||
zeigeFaellig?: boolean;
|
||||
jetzt: Date;
|
||||
}) {
|
||||
/*
|
||||
* Rot nur fuer **verstrichene** Fristen, und verstrichen heisst „vor heute“.
|
||||
* „Mir zugewiesen“ zeigt auch Laeufe, die erst naechste Woche faellig sind;
|
||||
* ein „seit 26.09. faellig“ in Warnfarbe waere dort schlicht falsch — und wer
|
||||
* eine Warnung zweimal umsonst sieht, liest die dritte nicht mehr. Dieselbe
|
||||
* Regel wie im Lesemodell, aus `src/lib/fristen.ts`.
|
||||
*/
|
||||
const ueberfaellig = fristVerstrichen(lauf.faelligAm, jetzt);
|
||||
const untertitel = [
|
||||
lauf.vorlagenName,
|
||||
lauf.geraetName,
|
||||
lauf.mitarbeiterName,
|
||||
lauf.zugewiesenAn ? `für ${lauf.zugewiesenAn}` : null,
|
||||
]
|
||||
.filter((stueck): stueck is string => Boolean(stueck))
|
||||
.join(" · ");
|
||||
|
||||
return (
|
||||
<li className="border-t border-linie first:border-t-0">
|
||||
<Link href={`/laeufe/${lauf.id}`} className="block py-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="min-w-0 font-medium break-words">{lauf.titel}</span>
|
||||
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">
|
||||
{STATUS_TEXT[lauf.status]}
|
||||
</span>
|
||||
{zeigeFaellig && lauf.faelligAm ? (
|
||||
<span
|
||||
className={`flex items-center gap-1 rounded-pille px-2.5 py-1 text-xs ${
|
||||
ueberfaellig ? "bg-fehler/15 text-fehler" : "bg-flaeche-hoch text-nebentext"
|
||||
}`}
|
||||
>
|
||||
<Clock className="size-3" strokeWidth={2} aria-hidden="true" />
|
||||
{ueberfaellig ? "seit" : "fällig am"} {DATUM.format(lauf.faelligAm)}
|
||||
{ueberfaellig ? " fällig" : ""}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-xs break-words text-nebentext">{untertitel}</p>
|
||||
<Fortschritt lauf={lauf} />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function Block({
|
||||
titel,
|
||||
symbol: Symbol,
|
||||
laeufe,
|
||||
leer,
|
||||
mehr,
|
||||
zeigeFaellig,
|
||||
warnend,
|
||||
jetzt,
|
||||
}: {
|
||||
titel: string;
|
||||
symbol: typeof ListChecks;
|
||||
laeufe: readonly UebersichtsLauf[];
|
||||
/** Was dasteht, wenn nichts dasteht. Ein leerer Kasten erklärt nichts. */
|
||||
leer: string;
|
||||
/**
|
||||
* Der Weg zum Rest.
|
||||
*
|
||||
* Die Beschriftung sagt, wohin es geht — die Laufliste kennt keinen Filter
|
||||
* „ueberfaellig“, und „weitere in der Liste“ haette Genaueres versprochen, als
|
||||
* dort zu finden ist (Reviewbefund zu #015).
|
||||
*/
|
||||
mehr?: { readonly anzahl: number; readonly pfad: string; readonly beschriftung: string };
|
||||
zeigeFaellig?: boolean;
|
||||
warnend?: boolean;
|
||||
jetzt: Date;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className={`rounded-karte border bg-flaeche p-5 ${warnend && laeufe.length > 0 ? "border-fehler/40" : "border-linie"}`}
|
||||
>
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold">
|
||||
{/*
|
||||
* Ein leerer Block ruft nicht: das Warndreieck ueber „nichts ist
|
||||
* ueberfaellig“ in Signal-Lime waere ein Ausrufezeichen ohne Satz.
|
||||
*/}
|
||||
<Symbol
|
||||
className={`size-5 ${
|
||||
laeufe.length === 0 ? "text-nebentext" : warnend ? "text-fehler" : "text-akzent"
|
||||
}`}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{titel}
|
||||
</h2>
|
||||
|
||||
{laeufe.length === 0 ? (
|
||||
<p className="mt-3 max-w-prose text-sm text-nebentext">{leer}</p>
|
||||
) : (
|
||||
<>
|
||||
<ul className="mt-2 flex flex-col">
|
||||
{laeufe.map((lauf) => (
|
||||
<LaufZeile key={lauf.id} lauf={lauf} zeigeFaellig={zeigeFaellig} jetzt={jetzt} />
|
||||
))}
|
||||
</ul>
|
||||
{mehr && mehr.anzahl > 0 ? (
|
||||
<Link
|
||||
href={mehr.pfad}
|
||||
className="mt-3 flex min-h-11 w-fit items-center gap-2 text-sm text-akzent underline underline-offset-2 md:min-h-10"
|
||||
>
|
||||
{mehr.anzahl} {mehr.anzahl === 1 ? "weiterer" : "weitere"} — {mehr.beschriftung}
|
||||
<ArrowRight className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||||
</Link>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Schnellstart({ vorlagen }: { vorlagen: readonly HaeufigeVorlage[] }) {
|
||||
if (vorlagen.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Rocket className="size-5 text-akzent" strokeWidth={2} aria-hidden="true" />
|
||||
Schnellstart
|
||||
</h2>
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">
|
||||
Die {vorlagen.length === 1 ? "Vorlage" : `${vorlagen.length} Vorlagen`}, aus denen hier am häufigsten
|
||||
Läufe entstehen.{" "}
|
||||
<Link href="/vorlagen" className="underline underline-offset-2 hover:text-text">
|
||||
Alle Vorlagen
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<ul className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
{vorlagen.map((vorlage) => (
|
||||
<li key={vorlage.versionId}>
|
||||
<Link
|
||||
href={`/laeufe/starten?vorlage=${vorlage.versionId}`}
|
||||
className="flex min-h-11 flex-col justify-center rounded-karte border border-linie px-4 py-3 hover:bg-flaeche-hoch md:min-h-10"
|
||||
>
|
||||
<span className="font-medium break-words">{vorlage.name}</span>
|
||||
<span className="mt-0.5 text-xs text-nebentext">
|
||||
{[
|
||||
vorlage.kategorie,
|
||||
`v${vorlage.version}`,
|
||||
vorlage.anzahlLaeufe === 0 ? "noch nie gestartet" : `${vorlage.anzahlLaeufe}× gestartet`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function Uebersichtsseite() {
|
||||
const { benutzer } = await sitzungErforderlich("lesen", "/");
|
||||
|
||||
const darfArbeiten = darf(benutzer.rolle, "laeufeBearbeiten");
|
||||
/*
|
||||
* **Ein** Zeitpunkt fuer die ganze Seite: die Abfrage und die Anzeige muessen
|
||||
* dieselbe Grenze benutzen, sonst stuende ein Lauf im Block „Ueberfaellig“ und
|
||||
* traege daneben eine unauffaellige Frist.
|
||||
*/
|
||||
const jetzt = new Date();
|
||||
const [daten, vorlagen] = await Promise.all([
|
||||
uebersicht(benutzer, jetzt),
|
||||
// Schnellstart nur für die, die auch starten dürfen.
|
||||
darfArbeiten ? haeufigeVorlagen(benutzer) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold">Übersicht</h1>
|
||||
<p className="mt-2 max-w-prose text-nebentext">
|
||||
Datenmodell und Anmeldung stehen. Als Nächstes kommen die Betriebsmodi und darauf die Vorlagen —
|
||||
danach lässt sich der erste Lauf wirklich abarbeiten.
|
||||
Guten Tag, {benutzer.name}. Was hier steht, ist nach Dringlichkeit sortiert — überfällig zuerst.
|
||||
</p>
|
||||
|
||||
<section className="mt-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{[
|
||||
{ nummer: "01", titel: "Betriebsmodus und Kunden", issue: "#008" },
|
||||
{ nummer: "02", titel: "Vorlagen-Versionierung", issue: "#009" },
|
||||
{ nummer: "03", titel: "Vorlagen-Editor", issue: "#010" },
|
||||
].map((karte) => (
|
||||
<article key={karte.nummer} className="rounded-karte border border-linie bg-flaeche p-5">
|
||||
<p className="mono text-xs text-nebentext">
|
||||
<span className="text-akzent">{karte.nummer}</span> · als Nächstes
|
||||
</p>
|
||||
<h2 className="mt-2 text-lg font-semibold">{karte.titel}</h2>
|
||||
<p className="mono mt-1 text-xs text-nebentext">{karte.issue}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<div className="mt-6 flex flex-col gap-4">
|
||||
<Block
|
||||
titel="Überfällig"
|
||||
symbol={TriangleAlert}
|
||||
laeufe={daten.ueberfaellig}
|
||||
leer="Nichts ist überfällig. Ein Lauf gilt als überfällig, sobald sein Fälligkeitsdatum verstrichen ist und er noch offen oder in Arbeit ist."
|
||||
mehr={{
|
||||
anzahl: daten.mehrUeberfaellig,
|
||||
pfad: "/laeufe",
|
||||
beschriftung: "die Laufliste sortiert nach Fälligkeit",
|
||||
}}
|
||||
zeigeFaellig
|
||||
warnend
|
||||
jetzt={jetzt}
|
||||
/>
|
||||
|
||||
<Block
|
||||
titel="Mir zugewiesen"
|
||||
symbol={ListChecks}
|
||||
laeufe={daten.meine}
|
||||
leer="Dir ist gerade nichts zugewiesen. Zugewiesen wird beim Anlegen eines Laufs oder auf der Geräteseite."
|
||||
mehr={{ anzahl: daten.mehrMeine, pfad: "/laeufe?meine=1", beschriftung: "alle meine Läufe" }}
|
||||
zeigeFaellig
|
||||
jetzt={jetzt}
|
||||
/>
|
||||
|
||||
<Block
|
||||
titel="Zuletzt bearbeitet"
|
||||
symbol={Clock}
|
||||
laeufe={daten.zuletzt}
|
||||
leer="Hier steht, woran zuletzt gearbeitet wurde — auch von Kolleginnen und Kollegen. Noch gibt es keinen Lauf."
|
||||
jetzt={jetzt}
|
||||
/>
|
||||
|
||||
{darfArbeiten ? <Schnellstart vorlagen={vorlagen} /> : null}
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-xs text-nebentext">
|
||||
Je Block höchstens {UEBERSICHT_JE_BLOCK} Läufe.{" "}
|
||||
<Link href="/laeufe" className="underline underline-offset-2 hover:text-text">
|
||||
Alle Läufe
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user