Files
arbeitszeit/components/CustomSelect.tsx
T
admin 01a7ac8e0b Initiale Arbeitszeiterfassung (Next.js 16 + SQLite)
Web-App zur Erfassung der Arbeitszeit, Layout/Architektur an Projekt logbuch
angelehnt, mit grüner Grundfarbe und SQLite3 statt MySQL.

- Eingabemaske: Datum (mit Wochentag), Ort (Kunde FFM/Homeoffice/Andrena),
  Beginn/Ende, Reisezeit, Vertriebsunterstützung (default 2h), Kommentar (max 500)
- Berechnung: Arbeitszeit = Ende-Beginn; Mehrarbeit = Arbeitszeit - 8h - Pause
- Pause als globale Einstellung (Einstellungen-Tab)
- Tabs Eingabe/Liste/Einstellungen, letzte 10 Einträge unter der Maske
- Single-User-Login (Benutzer Birgit + Passwort fest in .env), JWT-Cookie via jose
- better-sqlite3, Docker-Setup für docker.citysensor.de

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:53:44 +02:00

82 lines
2.6 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
interface Props {
options: SelectOption[];
placeholder: string;
onChange: (value: string) => void;
keepOpen?: boolean;
id?: string;
}
export default function CustomSelect({ options, placeholder, onChange, keepOpen = false, id }: Props) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener('mousedown', handleOutside);
return () => document.removeEventListener('mousedown', handleOutside);
}, [open]);
function select(value: string) {
if (!keepOpen) setOpen(false);
onChange(value);
}
return (
<div ref={ref} className="relative">
<button
id={id}
type="button"
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-green-600 focus:outline-none"
>
<span>{placeholder}</span>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && (
<div className="absolute z-50 left-0 right-0 mt-1 bg-white border-2 border-gray-400 rounded-lg shadow-lg max-h-72 overflow-y-auto">
{options.map((opt) => (
<button
key={opt.value}
type="button"
disabled={opt.disabled}
onClick={() => select(opt.value)}
className="w-full text-left px-4 py-2 text-base text-gray-900 hover:bg-green-50 active:bg-green-100 border-b border-gray-100 last:border-0 disabled:text-gray-400 disabled:bg-gray-50"
>
{opt.label}
</button>
))}
{keepOpen && (
<button
type="button"
onClick={() => setOpen(false)}
className="w-full px-4 py-2 text-base font-medium text-center bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-b-lg border-t-2 border-gray-300"
>
Fertig
</button>
)}
</div>
)}
</div>
);
}