feat: add password recovery and everyday workflow improvements
This commit is contained in:
@@ -4,6 +4,7 @@ import { useId, useState, useTransition, type ReactNode } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -32,6 +33,7 @@ export function Field({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
autoFocus,
|
||||
}: {
|
||||
label: string;
|
||||
name: string;
|
||||
@@ -41,6 +43,7 @@ export function Field({
|
||||
min?: string | number;
|
||||
max?: string | number;
|
||||
step?: string | number;
|
||||
autoFocus?: boolean;
|
||||
}) {
|
||||
const id = useId();
|
||||
return (
|
||||
@@ -57,6 +60,7 @@ export function Field({
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -123,7 +127,11 @@ function transform(command: string, form: FormData) {
|
||||
return {
|
||||
kind: text("kind"),
|
||||
startDate: text("startDate"),
|
||||
endDate: text("endDate"),
|
||||
endDate:
|
||||
text("kind") === "TIME_OFF" ||
|
||||
(text("kind") === "VACATION" && form.get("halfDay") === "on")
|
||||
? text("startDate")
|
||||
: text("endDate"),
|
||||
halfDay: form.get("halfDay") === "on",
|
||||
...(text("kind") === "TIME_OFF" ? { minutes: hours("hours") } : {}),
|
||||
note: text("note"),
|
||||
@@ -142,13 +150,19 @@ function StaffForm({
|
||||
children,
|
||||
label = "Speichern",
|
||||
onSuccess,
|
||||
anotherLabel,
|
||||
}: {
|
||||
command: string;
|
||||
profileId?: string;
|
||||
extra?: Record<string, unknown>;
|
||||
children: ReactNode;
|
||||
label?: string;
|
||||
onSuccess?: (result: StaffActionResult) => void;
|
||||
anotherLabel?: string;
|
||||
onSuccess?: (
|
||||
result: StaffActionResult,
|
||||
form: FormData,
|
||||
another: boolean,
|
||||
) => void;
|
||||
}) {
|
||||
const [pending, start] = useTransition();
|
||||
const [error, setError] = useState("");
|
||||
@@ -159,6 +173,10 @@ function StaffForm({
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const another =
|
||||
(event.nativeEvent as SubmitEvent).submitter?.getAttribute(
|
||||
"value",
|
||||
) === "another";
|
||||
setError("");
|
||||
start(async () => {
|
||||
try {
|
||||
@@ -173,7 +191,7 @@ function StaffForm({
|
||||
}
|
||||
if (result.warning) toast.warning(result.warning);
|
||||
else toast.success(result.message ?? "Gespeichert.");
|
||||
onSuccess?.(result);
|
||||
onSuccess?.(result, form, another);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
setError(
|
||||
@@ -187,10 +205,20 @@ function StaffForm({
|
||||
>
|
||||
<fieldset disabled={pending} className="grid gap-4 disabled:opacity-60">
|
||||
{children}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="submit">
|
||||
{pending ? "Wird gespeichert …" : label}
|
||||
</Button>
|
||||
{anotherLabel ? (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
name="intent"
|
||||
value="another"
|
||||
>
|
||||
{anotherLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
{error && (
|
||||
@@ -333,8 +361,12 @@ export function SetupForm({
|
||||
export function TimeForm({
|
||||
profileId,
|
||||
initial,
|
||||
defaultDate,
|
||||
compact = false,
|
||||
}: {
|
||||
profileId: string;
|
||||
defaultDate?: string;
|
||||
compact?: boolean;
|
||||
initial?: {
|
||||
id: string;
|
||||
date: string;
|
||||
@@ -345,14 +377,34 @@ export function TimeForm({
|
||||
};
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(initial?.date ?? defaultDate ?? todayKey());
|
||||
const [blockVersion, setBlockVersion] = useState(0);
|
||||
const [continuation, setContinuation] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (next) {
|
||||
setDate(initial?.date ?? defaultDate ?? todayKey());
|
||||
setContinuation(false);
|
||||
}
|
||||
setOpen(next);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant={initial ? "outline" : "default"}
|
||||
size={initial ? "sm" : "default"}
|
||||
variant={initial || compact ? "outline" : "default"}
|
||||
size={initial || compact ? "sm" : "default"}
|
||||
disabled={!initial && !!defaultDate && defaultDate > todayKey()}
|
||||
aria-label={
|
||||
compact ? `Arbeitszeit für ${defaultDate} erfassen` : undefined
|
||||
}
|
||||
>
|
||||
{initial ? "Bearbeiten" : "+ Arbeitszeit erfassen"}
|
||||
{initial
|
||||
? "Bearbeiten"
|
||||
: compact
|
||||
? "Zeit erfassen"
|
||||
: "+ Arbeitszeit erfassen"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
@@ -366,31 +418,50 @@ export function TimeForm({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<StaffForm
|
||||
key={blockVersion}
|
||||
command="time"
|
||||
profileId={profileId}
|
||||
extra={initial ? { id: initial.id } : {}}
|
||||
label="Arbeitszeit speichern"
|
||||
onSuccess={() => setOpen(false)}
|
||||
anotherLabel={
|
||||
!initial ? "Speichern und weiteren Block erfassen" : undefined
|
||||
}
|
||||
onSuccess={(_result, form, another) => {
|
||||
if (another && !initial) {
|
||||
setDate(String(form.get("date")));
|
||||
setContinuation(true);
|
||||
setBlockVersion((version) => version + 1);
|
||||
} else setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
label="Datum"
|
||||
name="date"
|
||||
type="date"
|
||||
value={initial?.date ?? todayKey()}
|
||||
value={date}
|
||||
max={todayKey()}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="Beginn"
|
||||
autoFocus={continuation}
|
||||
name="start"
|
||||
type="time"
|
||||
value={minuteClock(initial?.startMinute ?? 480)}
|
||||
value={
|
||||
continuation
|
||||
? undefined
|
||||
: minuteClock(initial?.startMinute ?? 480)
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
label="Ende"
|
||||
name="end"
|
||||
type="time"
|
||||
value={minuteClock(initial?.endMinute ?? 960)}
|
||||
value={
|
||||
continuation
|
||||
? undefined
|
||||
: minuteClock(initial?.endMinute ?? 960)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Field
|
||||
@@ -399,7 +470,7 @@ export function TimeForm({
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
value={initial?.breakMinutes ?? 30}
|
||||
value={continuation ? 0 : (initial?.breakMinutes ?? 30)}
|
||||
/>
|
||||
<Field
|
||||
label="Notiz (optional)"
|
||||
@@ -431,9 +502,24 @@ export function LeaveForm({
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [kind, setKind] = useState(initial?.kind ?? "VACATION");
|
||||
const [startDate, setStartDate] = useState(initial?.startDate ?? todayKey());
|
||||
const [endDate, setEndDate] = useState(initial?.endDate ?? todayKey());
|
||||
const [halfDay, setHalfDay] = useState(initial?.halfDay ?? false);
|
||||
const singleDay = kind === "TIME_OFF" || (kind === "VACATION" && halfDay);
|
||||
const id = useId();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (next) {
|
||||
setKind(initial?.kind ?? "VACATION");
|
||||
setStartDate(initial?.startDate ?? todayKey());
|
||||
setEndDate(initial?.endDate ?? todayKey());
|
||||
setHalfDay(initial?.halfDay ?? false);
|
||||
}
|
||||
setOpen(next);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button size={initial ? "sm" : "default"} variant="outline">
|
||||
{initial ? "Ändern" : "+ Abwesenheit beantragen"}
|
||||
@@ -474,33 +560,51 @@ export function LeaveForm({
|
||||
{admin && <option value="SICKNESS">Krankheit</option>}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
label="Von"
|
||||
name="startDate"
|
||||
type="date"
|
||||
value={initial?.startDate ?? todayKey()}
|
||||
/>
|
||||
<Field
|
||||
label="Bis"
|
||||
name="endDate"
|
||||
type="date"
|
||||
value={initial?.endDate ?? todayKey()}
|
||||
/>
|
||||
</div>
|
||||
{kind === "VACATION" && (
|
||||
{kind === "VACATION" ? (
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
name="halfDay"
|
||||
type="checkbox"
|
||||
defaultChecked={initial?.halfDay}
|
||||
/>
|
||||
Halber Urlaubstag (Von = Bis)
|
||||
checked={halfDay}
|
||||
onChange={(event) => setHalfDay(event.target.checked)}
|
||||
/>{" "}
|
||||
Halber Urlaubstag
|
||||
</label>
|
||||
)}
|
||||
) : null}
|
||||
<div className={singleDay ? "grid gap-4" : "grid grid-cols-2 gap-4"}>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor={`${id}-start`}>
|
||||
{singleDay ? "Datum" : "Von"}
|
||||
</Label>
|
||||
<Input
|
||||
id={`${id}-start`}
|
||||
name="startDate"
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(event) => setStartDate(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{singleDay ? (
|
||||
<input type="hidden" name="endDate" value={startDate} />
|
||||
) : (
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor={`${id}-end`}>Bis</Label>
|
||||
<Input
|
||||
id={`${id}-end`}
|
||||
name="endDate"
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(event) => setEndDate(event.target.value)}
|
||||
required
|
||||
min={startDate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{kind === "TIME_OFF" && (
|
||||
<Field
|
||||
label="Freizeitausgleich in Stunden (Von = Bis)"
|
||||
label="Freizeitausgleich in Stunden"
|
||||
name="hours"
|
||||
type="number"
|
||||
min={1 / 60}
|
||||
|
||||
@@ -284,7 +284,17 @@ export default async function PersonalPage({
|
||||
</div>
|
||||
{profile && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<TimeForm profileId={profile.id} />
|
||||
<TimeForm
|
||||
key={`${profile.id}-${start}`}
|
||||
profileId={profile.id}
|
||||
defaultDate={
|
||||
tab === "zeit"
|
||||
? today >= start && today <= end
|
||||
? today
|
||||
: start
|
||||
: today
|
||||
}
|
||||
/>
|
||||
<LeaveForm profileId={profile.id} admin={admin} />
|
||||
</div>
|
||||
)}
|
||||
@@ -645,6 +655,7 @@ export default async function PersonalPage({
|
||||
"Ist",
|
||||
"Urlaub / Krankheit",
|
||||
"Differenz",
|
||||
"Erfassen",
|
||||
].map((s) => (
|
||||
<th key={s} className="pb-3 pr-4 font-medium">
|
||||
{s}
|
||||
@@ -682,6 +693,15 @@ export default async function PersonalPage({
|
||||
>
|
||||
{formatMinutes(d.balance)}
|
||||
</td>
|
||||
<td className="py-3 pl-3">
|
||||
{editable && d.date <= today ? (
|
||||
<TimeForm
|
||||
profileId={profile.id}
|
||||
defaultDate={d.date}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -696,6 +716,7 @@ export default async function PersonalPage({
|
||||
<td className="pt-4">
|
||||
{formatMinutes(totals.balance)}
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user