Add birthday reminder widget to omsorgapp dashboard (FR-MA-7)
Also fixes date-input fields on the employee form (dateOfBirth/entryDate/ exitDate) to convert the generated client's Date objects into the "YYYY-MM-DD" string <input type="date"> expects — needed for the widget's birthday calculation and previously left the fields blank on edit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
43c82e655b
commit
99b5e07390
@@ -21,20 +21,31 @@ export const emptyEmployeeForm = {
|
||||
qualification: "",
|
||||
};
|
||||
|
||||
// Der generierte Client wandelt Datumsfelder aus der API-Antwort automatisch in Date-Objekte um
|
||||
// (siehe employeesApi.js) - <input type="date"> braucht dafür einen "YYYY-MM-DD"-String, sonst
|
||||
// bleibt das Feld leer (Muster analog AbsenceForm.jsx/OrderForm.jsx/TimeEntryForm.jsx).
|
||||
function toDateInputValue(value) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function employeeToFormValues(employee) {
|
||||
return {
|
||||
firstName: employee.firstName ?? "",
|
||||
lastName: employee.lastName ?? "",
|
||||
status: employee.status ?? "Aktiv",
|
||||
dateOfBirth: employee.dateOfBirth ?? "",
|
||||
dateOfBirth: toDateInputValue(employee.dateOfBirth),
|
||||
street: employee.street ?? "",
|
||||
postalCode: employee.postalCode ?? "",
|
||||
city: employee.city ?? "",
|
||||
country: employee.country ?? "",
|
||||
phoneNumber: employee.phoneNumber ?? "",
|
||||
email: employee.email ?? "",
|
||||
entryDate: employee.entryDate ?? "",
|
||||
exitDate: employee.exitDate ?? "",
|
||||
entryDate: toDateInputValue(employee.entryDate),
|
||||
exitDate: toDateInputValue(employee.exitDate),
|
||||
emergencyContactName: employee.emergencyContactName ?? "",
|
||||
emergencyContactPhone: employee.emergencyContactPhone ?? "",
|
||||
emergencyContactRelation: employee.emergencyContactRelation ?? "",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Cake } from "lucide-react";
|
||||
|
||||
import OmsorgCard from "../../components/OmsorgCard";
|
||||
import { useAuth } from "../../app/AuthContext";
|
||||
|
||||
const WIDGET_PAGE_SIZE = 200;
|
||||
const REMINDER_DAYS_AHEAD = 5;
|
||||
|
||||
// Nächstes Vorkommen des Geburtstags (dieses Jahr, oder nächstes Jahr falls schon vorbei) - Alter
|
||||
// spielt für die Erinnerung keine Rolle, nur der Tag/Monat.
|
||||
function daysUntilNextBirthday(dateOfBirth) {
|
||||
const birth = dateOfBirth instanceof Date ? dateOfBirth : new Date(dateOfBirth);
|
||||
if (Number.isNaN(birth.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
let next = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
|
||||
next.setHours(0, 0, 0, 0);
|
||||
if (next < today) {
|
||||
next = new Date(today.getFullYear() + 1, birth.getMonth(), birth.getDate());
|
||||
}
|
||||
|
||||
return Math.round((next - today) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
function remainingLabel(days) {
|
||||
if (days === 0) {
|
||||
return "heute";
|
||||
}
|
||||
if (days === 1) {
|
||||
return "morgen";
|
||||
}
|
||||
return `in ${days} Tagen`;
|
||||
}
|
||||
|
||||
export default function BirthdayWidget() {
|
||||
const { hasPermission } = useAuth();
|
||||
const canView = hasPermission("Employees", "View");
|
||||
|
||||
const [employees, setEmployees] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canView) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setIsLoading(true);
|
||||
const result = await window.omsorg.employees.list({ status: "Aktiv", page: 1, pageSize: WIDGET_PAGE_SIZE });
|
||||
|
||||
if (!cancelled) {
|
||||
setEmployees(result.ok ? result.data?.items ?? [] : []);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [canView]);
|
||||
|
||||
if (!canView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const upcoming = employees
|
||||
.filter((employee) => employee.dateOfBirth)
|
||||
.map((employee) => ({ employee, days: daysUntilNextBirthday(employee.dateOfBirth) }))
|
||||
.filter(({ days }) => days !== null && days >= 0 && days <= REMINDER_DAYS_AHEAD)
|
||||
.sort((a, b) => a.days - b.days);
|
||||
|
||||
return (
|
||||
<OmsorgCard title="Anstehende Geburtstage">
|
||||
<div className="contract-widget">
|
||||
<div className="contract-widget__intro">
|
||||
<Cake size={20} />
|
||||
|
||||
<p>Mitarbeiter, die in den nächsten {REMINDER_DAYS_AHEAD} Tagen Geburtstag haben.</p>
|
||||
</div>
|
||||
|
||||
{!isLoading && upcoming.length === 0 && <p>Keine anstehenden Geburtstage.</p>}
|
||||
|
||||
<div className="contract-widget__list">
|
||||
{upcoming.map(({ employee, days }) => (
|
||||
<article key={employee.id} className={`contract-item contract-item--${days === 0 ? "danger" : "info"}`}>
|
||||
<div className="contract-item__header">
|
||||
<div>
|
||||
<strong>
|
||||
{employee.firstName} {employee.lastName}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<strong>{remainingLabel(days)}</strong>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</OmsorgCard>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import HomeStats from "./HomeStats";
|
||||
import OmsorgBadge from "../../components/ui/OmsorgBadge";
|
||||
import FollowUpWidget from "./FollowUpWidget";
|
||||
import OrderStatusWidget from "./OrderStatusWidget";
|
||||
import BirthdayWidget from "./BirthdayWidget";
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
@@ -31,6 +32,7 @@ export default function HomePage() {
|
||||
<section className="dashboard-grid home-grid">
|
||||
<FollowUpWidget />
|
||||
<OrderStatusWidget />
|
||||
<BirthdayWidget />
|
||||
|
||||
<OmsorgCard title="Schnellaktionen">
|
||||
<div className="home-actions">
|
||||
|
||||
Reference in New Issue
Block a user