feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { HolidayCalendarEditor } from "~/components/vacations/HolidayCalendarEditor.js";
|
||||
import { PublicHolidayBatch } from "~/components/vacations/PublicHolidayBatch.js";
|
||||
import { EntitlementManager } from "~/components/vacations/EntitlementManager.js";
|
||||
|
||||
@@ -8,10 +9,40 @@ export default function AdminVacationsPage() {
|
||||
<div className="p-6 max-w-5xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Vacation Management</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Manage public holidays, entitlements, and year summaries</p>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Verwalte Feiertagskalender pro Land, Bundesland und Stadt sowie Entitlements und Fallback-Importe.
|
||||
</p>
|
||||
</div>
|
||||
<PublicHolidayBatch />
|
||||
<EntitlementManager />
|
||||
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">Holiday Calendars</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Fachliche Quelle fuer regionale Feiertage. Diese Kalender werden fuer Urlaubszaehlung, Timeline-Overlay und Assistant-Abfragen verwendet.
|
||||
</p>
|
||||
</div>
|
||||
<HolidayCalendarEditor />
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">Legacy Batch Import</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Nur als Fallback fuer bestaende Prozesse. Bevorzugt sollen Feiertage ueber die Kalenderlogik und nicht als statische Urlaubseintraege gepflegt werden.
|
||||
</p>
|
||||
</div>
|
||||
<PublicHolidayBatch />
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">Entitlements</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Jahresansprueche und Resttage im gleichen Kontext pruefen, nachdem Feiertage regional aufgeloest wurden.
|
||||
</p>
|
||||
</div>
|
||||
<EntitlementManager />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { usePermissions } from "~/hooks/usePermissions.js";
|
||||
import { useColumnConfig } from "~/hooks/useColumnConfig.js";
|
||||
import { useViewPrefs } from "~/hooks/useViewPrefs.js";
|
||||
import { useRowOrder } from "~/hooks/useRowOrder.js";
|
||||
import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js";
|
||||
import { DraggableTableRow } from "~/components/ui/DraggableTableRow.js";
|
||||
|
||||
type ModalState =
|
||||
@@ -85,68 +86,22 @@ function FilterDropdown({
|
||||
tooltipContent?: ReactNode;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const [panelPosition, setPanelPosition] = useState({ top: 0, left: 0, minWidth: 0 });
|
||||
|
||||
const updatePanelPosition = useCallback(() => {
|
||||
const trigger = dropdownRef.current;
|
||||
if (!trigger) return;
|
||||
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
const panelWidth = panelRef.current?.offsetWidth ?? rect.width;
|
||||
const viewportPadding = 16;
|
||||
const maxLeft = Math.max(viewportPadding, window.innerWidth - panelWidth - viewportPadding);
|
||||
|
||||
setPanelPosition({
|
||||
top: rect.bottom + 8,
|
||||
left: Math.min(Math.max(rect.left, viewportPadding), maxLeft),
|
||||
minWidth: rect.width,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerDown(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
if (dropdownRef.current?.contains(target) || panelRef.current?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handlePointerDown);
|
||||
return () => document.removeEventListener("mousedown", handlePointerDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
updatePanelPosition();
|
||||
const rafId = window.requestAnimationFrame(updatePanelPosition);
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", updatePanelPosition);
|
||||
window.addEventListener("scroll", updatePanelPosition, true);
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
window.removeEventListener("resize", updatePanelPosition);
|
||||
window.removeEventListener("scroll", updatePanelPosition, true);
|
||||
window.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [isOpen, updatePanelPosition]);
|
||||
const { triggerRef, panelRef, position, handleOpenChange } = useAnchoredOverlay<HTMLDivElement>({
|
||||
open: isOpen,
|
||||
onClose: () => setIsOpen(false),
|
||||
matchTriggerWidth: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="relative">
|
||||
<div ref={triggerRef} className="relative">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen((current) => !current)}
|
||||
onClick={() => {
|
||||
const nextOpen = !isOpen;
|
||||
handleOpenChange(nextOpen);
|
||||
setIsOpen(nextOpen);
|
||||
}}
|
||||
className={`inline-flex items-center justify-between gap-3 rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800 ${buttonClassName}`}
|
||||
>
|
||||
<span className="text-left">{label}</span>
|
||||
@@ -160,9 +115,9 @@ function FilterDropdown({
|
||||
ref={panelRef}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: panelPosition.top,
|
||||
left: panelPosition.left,
|
||||
minWidth: panelPosition.minWidth,
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
minWidth: position.minWidth,
|
||||
}}
|
||||
className={`z-[9998] rounded-2xl border border-gray-200 bg-white p-3 shadow-xl dark:border-gray-700 dark:bg-gray-900 ${widthClassName}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user