feat: Objekte-Kategorien (stern/sonne/beide) — Version 1.10.1

- Neue Spalte Kategorie (SET stern/sonne) in objekte-Tabelle
- ObjektSelector zeigt je nach ArtFuehrung nur passende Objekte
- SonnenFührung: Sonne fest vorausgewählt, zusätzliche Sonne-Objekte wählbar
- Bestehende Objekte erhalten Kategorie automatisch beim Speichern
- Admin: Kategorie editierbar (stern / sonne / stern,sonne)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 16:14:52 +02:00
parent 96ba03b909
commit 7475d4fd37
8 changed files with 92 additions and 42 deletions
+19 -6
View File
@@ -6,9 +6,11 @@ import type { ObjektOption, SelectedObjekt } from '@/types/logbuch';
interface Props {
selected: SelectedObjekt[];
onChange: (objekte: SelectedObjekt[]) => void;
kategorie?: 'stern' | 'sonne';
fixedItems?: SelectedObjekt[];
}
export default function ObjektSelector({ selected, onChange }: Props) {
export default function ObjektSelector({ selected, onChange, kategorie = 'stern', fixedItems = [] }: Props) {
const [all, setAll] = useState<ObjektOption[]>([]);
const [search, setSearch] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -16,11 +18,11 @@ export default function ObjektSelector({ selected, onChange }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetch('/api/objekte')
fetch('/api/objekte?kategorie=' + kategorie)
.then((r) => { if (!r.ok) throw new Error('Fehler'); return r.json(); })
.then(setAll)
.catch(() => {});
}, []);
}, [kategorie]);
useEffect(() => {
function handleOutside(e: MouseEvent) {
@@ -32,14 +34,17 @@ export default function ObjektSelector({ selected, onChange }: Props) {
return () => document.removeEventListener('mousedown', handleOutside);
}, [dropdownOpen]);
const fixedNamesLower = new Set(fixedItems.map((o) => o.Name.toLowerCase()));
const selectedNames = new Set(selected.map((o) => o.Name.toLowerCase()));
const available = all.filter((o) => !selectedNames.has(o.Name.toLowerCase()));
const available = all.filter(
(o) => !selectedNames.has(o.Name.toLowerCase()) && !fixedNamesLower.has(o.Name.toLowerCase())
);
const filtered = search
? available.filter((o) => o.Name.toLowerCase().startsWith(search.toLowerCase()))
: available;
const searchTrimmed = search.trim();
const alreadySelected = searchTrimmed !== '' && selectedNames.has(searchTrimmed.toLowerCase());
const alreadySelected = searchTrimmed !== '' && (selectedNames.has(searchTrimmed.toLowerCase()) || fixedNamesLower.has(searchTrimmed.toLowerCase()));
const exactAvailableMatch = available.find((o) => o.Name.toLowerCase() === searchTrimmed.toLowerCase());
const showAddNew = searchTrimmed !== '' && !alreadySelected && !exactAvailableMatch;
@@ -51,7 +56,7 @@ export default function ObjektSelector({ selected, onChange }: Props) {
function addNew(name: string) {
const trimmed = name.trim();
if (!trimmed || selectedNames.has(trimmed.toLowerCase())) return;
if (!trimmed || selectedNames.has(trimmed.toLowerCase()) || fixedNamesLower.has(trimmed.toLowerCase())) return;
const existing = all.find((o) => o.Name.toLowerCase() === trimmed.toLowerCase());
if (existing) {
onChange([...selected, { ID: existing.ID, Name: existing.Name }]);
@@ -79,6 +84,14 @@ export default function ObjektSelector({ selected, onChange }: Props) {
return (
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{fixedItems.map((o) => (
<span
key={'fixed-' + o.Name}
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-base px-3 py-1.5 rounded-full"
>
{o.Name}
</span>
))}
{selected.map((o) => (
<span
key={o.Name}