Durchführender: Verfügbarkeit pro Termin, Umbenennung Trainer→Durchführender

This commit is contained in:
rene 2026-05-20 19:43:46 +02:00
parent 59aa3cbcce
commit c23ac90d35
4 changed files with 213 additions and 117 deletions

View file

@ -43,6 +43,7 @@ export interface Mitglied {
} }
export type Rolle = 'admin' | 'trainer'; export type Rolle = 'admin' | 'trainer';
export type Verfuegbarkeit = 'offen' | 'bestaetigt' | 'abgesagt' | 'vertretung_gesucht';
export interface Gruppe { export interface Gruppe {
id: string; id: string;
@ -87,6 +88,8 @@ export interface Termin {
ende?: string; ende?: string;
ort?: string; ort?: string;
gruppe_ids: string[]; gruppe_ids: string[];
durchfuehrender_id?: string;
verfuegbarkeit?: Verfuegbarkeit;
} }
export interface Nachricht { export interface Nachricht {

View file

@ -208,7 +208,7 @@
{#if isAdmin()} {#if isAdmin()}
<section> <section>
<h2>Trainer</h2> <h2>Durchführende</h2>
{#if trainer.length === 0} {#if trainer.length === 0}
<p class="sepa-hint">Noch keine Trainer eingeladen.</p> <p class="sepa-hint">Noch keine Trainer eingeladen.</p>
{:else} {:else}

View file

@ -1,25 +1,29 @@
<script lang="ts"> <script lang="ts">
import { pb } from '$lib/pb'; import { pb } from '$lib/pb';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Termin, Gruppe } from '$lib/types'; import type { Termin, Gruppe, Verfuegbarkeit } from '$lib/types';
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
const userId = () => pb.authStore.record?.id as string;
let termine = $state<Termin[]>([]); let termine = $state<Termin[]>([]);
let gruppen = $state<Gruppe[]>([]); let gruppen = $state<Gruppe[]>([]);
let alleUser = $state<any[]>([]);
let loading = $state(true); let loading = $state(true);
// Formular // Formular
let showForm = $state(false); let showForm = $state(false);
let editId = $state<string | null>(null); let editId = $state<string | null>(null);
let fTitel = $state(''); let fTitel = $state('');
let fBeschr = $state(''); let fBeschr = $state('');
let fBeginn = $state(''); let fBeginn = $state('');
let fEnde = $state(''); let fEnde = $state('');
let fOrt = $state(''); let fOrt = $state('');
let fGruppeIds = $state<string[]>([]); let fGruppeIds = $state<string[]>([]);
let saving = $state(false); let fDurchfuehrenderId = $state('');
let formError = $state(''); let saving = $state(false);
let formError = $state('');
let showDelete = $state<string | null>(null); let showDelete = $state<string | null>(null);
const now = new Date(); const now = new Date();
@ -34,10 +38,19 @@
.sort((a, b) => new Date(b.beginn).getTime() - new Date(a.beginn).getTime()) .sort((a, b) => new Date(b.beginn).getTime() - new Date(a.beginn).getTime())
); );
// Admin-Warnung: Termine ohne Bestätigung
const offene = $derived(
upcoming.filter(t => !t.verfuegbarkeit || t.verfuegbarkeit === 'offen' || t.verfuegbarkeit === 'vertretung_gesucht')
);
onMount(async () => { onMount(async () => {
[termine, gruppen] = await Promise.all([ const vid = pb.authStore.record?.verein_id as string;
[termine, gruppen, alleUser] = await Promise.all([
pb.collection('termine').getFullList<Termin>({ sort: 'beginn' }), pb.collection('termine').getFullList<Termin>({ sort: 'beginn' }),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }), pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
isAdmin()
? pb.collection('users').getFullList({ filter: `verein_id = "${vid}" && rolle = "trainer"` })
: Promise.resolve([]),
]); ]);
loading = false; loading = false;
}); });
@ -45,47 +58,38 @@
function toLocal(iso: string | undefined): string { function toLocal(iso: string | undefined): string {
if (!iso) return ''; if (!iso) return '';
const d = new Date(iso); const d = new Date(iso);
// datetime-local format: YYYY-MM-DDTHH:MM
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16); return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
} }
function fromLocal(local: string): string { function fromLocal(local: string): string {
return local ? new Date(local).toISOString() : ''; return local ? new Date(local).toISOString() : '';
} }
function neuerTermin() { function neuerTermin() {
editId = null; editId = null; fTitel = ''; fBeschr = ''; fBeginn = ''; fEnde = '';
fTitel = ''; fBeschr = ''; fBeginn = ''; fEnde = ''; fOrt = ''; fGruppeIds = []; fOrt = ''; fGruppeIds = []; fDurchfuehrenderId = '';
formError = ''; showForm = true; formError = ''; showForm = true;
} }
function bearbeiten(t: Termin) { function bearbeiten(t: Termin) {
editId = t.id; editId = t.id; fTitel = t.titel; fBeschr = t.beschreibung ?? '';
fTitel = t.titel; fBeginn = toLocal(t.beginn); fEnde = toLocal(t.ende);
fBeschr = t.beschreibung ?? ''; fOrt = t.ort ?? ''; fGruppeIds = t.gruppe_ids ?? [];
fBeginn = toLocal(t.beginn); fDurchfuehrenderId = t.durchfuehrender_id ?? '';
fEnde = toLocal(t.ende);
fOrt = t.ort ?? '';
fGruppeIds = t.gruppe_ids ?? [];
formError = ''; showForm = true; formError = ''; showForm = true;
} }
async function speichern() { async function speichern() {
if (!fTitel.trim() || !fBeginn) { if (!fTitel.trim() || !fBeginn) { formError = 'Titel und Beginn sind Pflichtfelder.'; return; }
formError = 'Titel und Beginn sind Pflichtfelder.';
return;
}
formError = ''; saving = true; formError = ''; saving = true;
try { try {
const vid = pb.authStore.record?.verein_id as string; const vid = pb.authStore.record?.verein_id as string;
const data = { const data = {
verein_id: vid, verein_id: vid, titel: fTitel.trim(),
titel: fTitel.trim(),
beschreibung: fBeschr.trim() || null, beschreibung: fBeschr.trim() || null,
beginn: fromLocal(fBeginn), beginn: fromLocal(fBeginn), ende: fEnde ? fromLocal(fEnde) : null,
ende: fEnde ? fromLocal(fEnde) : null, ort: fOrt.trim() || null, gruppe_ids: fGruppeIds,
ort: fOrt.trim() || null, durchfuehrender_id: fDurchfuehrenderId || null,
gruppe_ids: fGruppeIds, verfuegbarkeit: fDurchfuehrenderId ? 'offen' : null,
}; };
if (editId) { if (editId) {
const updated = await pb.collection('termine').update<Termin>(editId, data); const updated = await pb.collection('termine').update<Termin>(editId, data);
@ -108,6 +112,11 @@
showDelete = null; showDelete = null;
} }
async function setVerfuegbarkeit(t: Termin, v: Verfuegbarkeit) {
const updated = await pb.collection('termine').update<Termin>(t.id, { verfuegbarkeit: v });
termine = termine.map(x => x.id === t.id ? updated : x);
}
function toggleGruppe(id: string) { function toggleGruppe(id: string) {
fGruppeIds = fGruppeIds.includes(id) fGruppeIds = fGruppeIds.includes(id)
? fGruppeIds.filter(g => g !== id) ? fGruppeIds.filter(g => g !== id)
@ -119,14 +128,27 @@
weekday: 'short', day: '2-digit', month: 'long', year: 'numeric', weekday: 'short', day: '2-digit', month: 'long', year: 'numeric',
}); });
} }
function formatZeit(iso: string): string { function formatZeit(iso: string): string {
return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} }
function gruppenLabel(ids: string[]): string { function gruppenLabel(ids: string[]): string {
if (!ids?.length) return ''; return (ids ?? []).map(id => gruppen.find(g => g.id === id)?.name).filter(Boolean).join(', ');
return ids.map(id => gruppen.find(g => g.id === id)?.name).filter(Boolean).join(', '); }
function userName(uid: string | undefined): string {
if (!uid) return '';
const u = alleUser.find((u: any) => u.id === uid);
return u?.name ?? '';
}
const verfuegbarkeitConfig: Record<Verfuegbarkeit, { label: string; farbe: string }> = {
offen: { label: 'Offen', farbe: '#f59e0b' },
bestaetigt: { label: 'Bestätigt', farbe: '#16a34a' },
abgesagt: { label: 'Abgesagt', farbe: '#dc2626' },
vertretung_gesucht: { label: 'Vertretung gesucht', farbe: '#7c3aed' },
};
function istMeinTermin(t: Termin): boolean {
return t.durchfuehrender_id === userId();
} }
</script> </script>
@ -134,13 +156,21 @@
<div class="top"> <div class="top">
<h1>Termine</h1> <h1>Termine</h1>
<button class="btn-primary" onclick={neuerTermin}>+ Termin</button> {#if isAdmin()}
<button class="btn-primary" onclick={neuerTermin}>+ Termin</button>
{/if}
</div> </div>
{#if isAdmin() && offene.length > 0}
<div class="warnung">
{offene.length} {offene.length === 1 ? 'Termin benötigt' : 'Termine benötigen'} eine Bestätigung
</div>
{/if}
{#if loading} {#if loading}
<p class="hint">Laden…</p> <p class="hint">Laden…</p>
{:else if termine.length === 0} {:else if termine.length === 0}
<p class="hint">Noch keine Termine lege den ersten an!</p> <p class="hint">Noch keine Termine geplant.</p>
{:else} {:else}
{#if upcoming.length > 0} {#if upcoming.length > 0}
<ul class="liste"> <ul class="liste">
@ -151,19 +181,58 @@
<span class="tag-zahl">{new Date(t.beginn).getDate()}</span> <span class="tag-zahl">{new Date(t.beginn).getDate()}</span>
<span class="monat">{new Date(t.beginn).toLocaleDateString('de-DE', { month: 'short' })}</span> <span class="monat">{new Date(t.beginn).toLocaleDateString('de-DE', { month: 'short' })}</span>
</div> </div>
<div class="karte-info"> <div class="karte-info">
<span class="karte-titel">{t.titel}</span> <span class="karte-titel">{t.titel}</span>
<span class="karte-meta"> <span class="karte-meta">
{formatZeit(t.beginn)}{t.ende ? ' ' + formatZeit(t.ende) : ''}{t.ort ? ' · ' + t.ort : ''} {formatZeit(t.beginn)}{t.ende ? ' ' + formatZeit(t.ende) : ''}{t.ort ? ' · ' + t.ort : ''}
</span> </span>
{#if t.gruppe_ids?.length} {#if t.gruppe_ids?.length}
<span class="karte-gruppen">{gruppenLabel(t.gruppe_ids)}</span> <span class="karte-sub">{gruppenLabel(t.gruppe_ids)}</span>
{/if}
{#if t.durchfuehrender_id}
<div class="verfueg-zeile">
{#if t.verfuegbarkeit && t.verfuegbarkeit !== 'offen'}
<span class="verfueg-badge" style="color:{verfuegbarkeitConfig[t.verfuegbarkeit].farbe}">
{verfuegbarkeitConfig[t.verfuegbarkeit].label}
</span>
{:else}
<span class="verfueg-badge" style="color:#f59e0b">● Offen</span>
{/if}
{#if isAdmin() && userName(t.durchfuehrender_id)}
<span class="karte-sub">{userName(t.durchfuehrender_id)}</span>
{/if}
</div>
{#if istMeinTermin(t)}
<div class="meine-aktionen">
<button
class="btn-aktion bestaetigen"
class:aktiv={t.verfuegbarkeit === 'bestaetigt'}
onclick={() => setVerfuegbarkeit(t, 'bestaetigt')}
>Ich bin dabei</button>
<button
class="btn-aktion absagen"
class:aktiv={t.verfuegbarkeit === 'abgesagt'}
onclick={() => setVerfuegbarkeit(t, 'abgesagt')}
>Kann nicht</button>
<button
class="btn-aktion vertretung"
class:aktiv={t.verfuegbarkeit === 'vertretung_gesucht'}
onclick={() => setVerfuegbarkeit(t, 'vertretung_gesucht')}
>Vertretung gesucht</button>
</div>
{/if}
{/if} {/if}
</div> </div>
<div class="karte-aktionen">
<button class="btn-icon" onclick={() => bearbeiten(t)} title="Bearbeiten"></button> {#if isAdmin()}
<button class="btn-icon btn-icon-red" onclick={() => showDelete = t.id} title="Löschen"></button> <div class="karte-aktionen">
</div> <button class="btn-icon" onclick={() => bearbeiten(t)} title="Bearbeiten"></button>
<button class="btn-icon btn-icon-red" onclick={() => showDelete = t.id} title="Löschen"></button>
</div>
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
@ -184,9 +253,11 @@
<span class="karte-titel">{t.titel}</span> <span class="karte-titel">{t.titel}</span>
<span class="karte-meta">{formatDatum(t.beginn)}{t.ort ? ' · ' + t.ort : ''}</span> <span class="karte-meta">{formatDatum(t.beginn)}{t.ort ? ' · ' + t.ort : ''}</span>
</div> </div>
<div class="karte-aktionen"> {#if isAdmin()}
<button class="btn-icon btn-icon-red" onclick={() => showDelete = t.id} title="Löschen"></button> <div class="karte-aktionen">
</div> <button class="btn-icon btn-icon-red" onclick={() => showDelete = t.id} title="Löschen"></button>
</div>
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
@ -194,18 +265,16 @@
{/if} {/if}
{/if} {/if}
<!-- Termin-Formular --> <!-- Termin-Formular (nur Admin) -->
{#if showForm} {#if showForm && isAdmin()}
<div class="overlay" role="dialog" aria-modal="true"> <div class="overlay" role="dialog" aria-modal="true">
<div class="sheet"> <div class="sheet">
<h2>{editId ? 'Termin bearbeiten' : 'Neuer Termin'}</h2> <h2>{editId ? 'Termin bearbeiten' : 'Neuer Termin'}</h2>
<form onsubmit={(e) => { e.preventDefault(); speichern(); }}> <form onsubmit={(e) => { e.preventDefault(); speichern(); }}>
<div class="field"> <div class="field">
<label for="ftitel">Titel *</label> <label for="ftitel">Titel *</label>
<input id="ftitel" type="text" bind:value={fTitel} placeholder="z. B. Jahreshauptversammlung" required /> <input id="ftitel" type="text" bind:value={fTitel} required />
</div> </div>
<div class="row"> <div class="row">
<div class="field"> <div class="field">
<label for="fbeginn">Beginn *</label> <label for="fbeginn">Beginn *</label>
@ -216,17 +285,23 @@
<input id="fende" type="datetime-local" bind:value={fEnde} /> <input id="fende" type="datetime-local" bind:value={fEnde} />
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="fort">Ort</label> <label for="fort">Ort</label>
<input id="fort" type="text" bind:value={fOrt} placeholder="z. B. Vereinsheim" /> <input id="fort" type="text" bind:value={fOrt} />
</div> </div>
<div class="field"> <div class="field">
<label for="fbeschr">Beschreibung</label> <label for="fbeschr">Beschreibung</label>
<textarea id="fbeschr" bind:value={fBeschr} rows="3" placeholder="Optional"></textarea> <textarea id="fbeschr" bind:value={fBeschr} rows="2"></textarea>
</div>
<div class="field">
<label for="fdurch">Durchführender</label>
<select id="fdurch" bind:value={fDurchfuehrenderId}>
<option value="">— nicht zugewiesen —</option>
{#each alleUser as u (u.id)}
<option value={u.id}>{u.name}</option>
{/each}
</select>
</div> </div>
{#if gruppen.length > 0} {#if gruppen.length > 0}
<div class="field"> <div class="field">
<span class="field-label">Für Gruppen</span> <span class="field-label">Für Gruppen</span>
@ -240,23 +315,17 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if formError}<p class="error">{formError}</p>{/if}
{#if formError}
<p class="error">{formError}</p>
{/if}
<div class="actions"> <div class="actions">
<button type="button" class="btn-ghost" onclick={() => showForm = false}>Abbrechen</button> <button type="button" class="btn-ghost" onclick={() => showForm = false}>Abbrechen</button>
<button type="submit" class="btn-primary" disabled={saving}> <button type="submit" class="btn-primary" disabled={saving}>{saving ? 'Speichern…' : 'Speichern'}</button>
{saving ? 'Speichern…' : 'Speichern'}
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
{/if} {/if}
<!-- Lösch-Bestätigung --> <!-- Lösch-Dialog -->
{#if showDelete} {#if showDelete}
<div class="overlay" role="dialog" aria-modal="true"> <div class="overlay" role="dialog" aria-modal="true">
<div class="dialog"> <div class="dialog">
@ -271,62 +340,75 @@
{/if} {/if}
<style> <style>
.top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; } .top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
h1 { font-size: 1.4rem; font-weight: 700; color: #1e293b; } h1 { font-size: 1.4rem; font-weight: 700; color: #1e293b; }
.hint { color: #94a3b8; font-size: 0.95rem; text-align: center; margin-top: 3rem; } .hint { color: #94a3b8; font-size: 0.95rem; text-align: center; margin-top: 3rem; }
.warnung {
background: #fffbeb; border: 1px solid #fde68a;
border-radius: 8px; padding: 0.65rem 0.9rem;
font-size: 0.85rem; color: #92400e; margin-bottom: 1rem;
}
.liste { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; } .liste { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.karte { .karte {
display: flex; display: flex; align-items: flex-start; gap: 0.75rem;
align-items: flex-start; padding: 0.85rem 1rem; background: #fff;
gap: 0.75rem; border: 1px solid #e2e8f0; border-radius: 10px;
padding: 0.85rem 1rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
} }
.karte-grau { background: #f8fafc; opacity: 0.75; } .karte-grau { background: #f8fafc; opacity: 0.75; }
.karte-datum-col { .karte-datum-col {
display: flex; display: flex; flex-direction: column; align-items: center;
flex-direction: column; min-width: 2.2rem; padding-top: 0.1rem;
align-items: center;
min-width: 2.2rem;
padding-top: 0.1rem;
} }
.tag { font-size: 0.65rem; font-weight: 600; color: #94a3b8; text-transform: uppercase; } .tag { font-size: 0.65rem; font-weight: 600; color: #94a3b8; text-transform: uppercase; }
.tag-zahl { font-size: 1.4rem; font-weight: 700; color: #1e40af; line-height: 1.1; } .tag-zahl { font-size: 1.4rem; font-weight: 700; color: #1e40af; line-height: 1.1; }
.monat { font-size: 0.65rem; color: #94a3b8; text-transform: uppercase; } .monat { font-size: 0.65rem; color: #94a3b8; text-transform: uppercase; }
.karte-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; } .karte-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.karte-titel { font-weight: 600; font-size: 0.95rem; color: #1e293b; } .karte-titel { font-weight: 600; font-size: 0.95rem; color: #1e293b; }
.karte-meta { font-size: 0.78rem; color: #64748b; } .karte-meta { font-size: 0.78rem; color: #64748b; }
.karte-gruppen { font-size: 0.72rem; color: #94a3b8; } .karte-sub { font-size: 0.72rem; color: #94a3b8; }
.verfueg-zeile { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.2rem; }
.verfueg-badge { font-size: 0.75rem; font-weight: 600; }
.meine-aktionen {
display: flex; gap: 0.35rem; flex-wrap: wrap; margin-top: 0.4rem;
}
.btn-aktion {
padding: 0.3rem 0.65rem; border-radius: 20px;
font-size: 0.75rem; font-weight: 600; cursor: pointer;
border: 1.5px solid #e2e8f0; background: #fff;
transition: all 0.15s;
}
.btn-aktion.bestaetigen { color: #16a34a; }
.btn-aktion.bestaetigen.aktiv { background: #dcfce7; border-color: #16a34a; }
.btn-aktion.absagen { color: #dc2626; }
.btn-aktion.absagen.aktiv { background: #fee2e2; border-color: #dc2626; }
.btn-aktion.vertretung { color: #7c3aed; }
.btn-aktion.vertretung.aktiv { background: #ede9fe; border-color: #7c3aed; }
.karte-aktionen { display: flex; gap: 0.35rem; flex-shrink: 0; } .karte-aktionen { display: flex; gap: 0.35rem; flex-shrink: 0; }
.btn-icon { .btn-icon {
width: 1.9rem; height: 1.9rem; width: 1.9rem; height: 1.9rem; display: flex; align-items: center;
display: flex; align-items: center; justify-content: center; justify-content: center; background: none; border: 1px solid #e2e8f0;
background: none; border: 1px solid #e2e8f0; border-radius: 6px; border-radius: 6px; color: #64748b; font-size: 0.85rem; cursor: pointer;
color: #64748b; font-size: 0.85rem; cursor: pointer;
transition: border-color 0.15s, color 0.15s;
} }
.btn-icon:hover { border-color: #94a3b8; color: #1e293b; } .btn-icon:hover { border-color: #94a3b8; color: #1e293b; }
.btn-icon-red:hover { border-color: #fca5a5; color: #dc2626; } .btn-icon-red:hover { border-color: #fca5a5; color: #dc2626; }
.vergangen { margin-top: 0.5rem; }
.vergangen summary { .vergangen summary {
font-size: 0.85rem; color: #94a3b8; cursor: pointer; font-size: 0.85rem; color: #94a3b8; cursor: pointer;
padding: 0.5rem 0; list-style: none; user-select: none; padding: 0.5rem 0; list-style: none; user-select: none;
} }
.vergangen-liste { margin-top: 0.5rem; } .vergangen-liste { margin-top: 0.5rem; }
/* Sheet & Overlay */ /* Sheet */
.overlay { .overlay {
position: fixed; inset: 0; position: fixed; inset: 0; background: rgba(0,0,0,0.4);
background: rgba(0,0,0,0.4);
display: flex; align-items: flex-end; justify-content: center; display: flex; align-items: flex-end; justify-content: center;
z-index: 100; padding: 1rem; z-index: 100; padding: 1rem;
padding-bottom: calc(1rem + env(safe-area-inset-bottom)); padding-bottom: calc(1rem + env(safe-area-inset-bottom));
@ -336,35 +418,26 @@
width: 100%; max-width: 480px; max-height: 92dvh; overflow-y: auto; width: 100%; max-width: 480px; max-height: 92dvh; overflow-y: auto;
} }
h2 { font-size: 1.1rem; font-weight: 700; color: #1e293b; margin-bottom: 1rem; } h2 { font-size: 1.1rem; font-weight: 700; color: #1e293b; margin-bottom: 1rem; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; } .row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.field { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 0.9rem; } .field { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 0.9rem; }
label, .field-label { font-size: 0.875rem; font-weight: 500; color: #475569; } label, .field-label { font-size: 0.875rem; font-weight: 500; color: #475569; }
input, textarea, select {
input, textarea { padding: 0.65rem 0.85rem; border: 1.5px solid #e2e8f0; border-radius: 8px;
padding: 0.65rem 0.85rem;
border: 1.5px solid #e2e8f0; border-radius: 8px;
font-size: 1rem; background: #fff; width: 100%; font-size: 1rem; background: #fff; width: 100%;
box-sizing: border-box; font-family: inherit; box-sizing: border-box; font-family: inherit; resize: vertical;
transition: border-color 0.15s; resize: vertical; transition: border-color 0.15s;
} }
input:focus, textarea:focus { outline: none; border-color: #1e40af; } input:focus, textarea:focus, select:focus { outline: none; border-color: #1e40af; }
.checkboxes { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.2rem; } .checkboxes { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.2rem; }
.check-label { .check-label {
display: flex; align-items: center; gap: 0.35rem; display: flex; align-items: center; gap: 0.35rem;
padding: 0.35rem 0.7rem; padding: 0.35rem 0.7rem; border: 1.5px solid #e2e8f0;
border: 1.5px solid #e2e8f0; border-radius: 20px; border-radius: 20px; font-size: 0.82rem; cursor: pointer;
font-size: 0.82rem; cursor: pointer;
transition: border-color 0.15s, background 0.15s;
} }
.check-label.active { border-color: #1e40af; background: #e0e7ff; color: #1e40af; } .check-label.active { border-color: #1e40af; background: #e0e7ff; color: #1e40af; }
.check-label input { display: none; } .check-label input { display: none; }
.error { color: #dc2626; font-size: 0.875rem; margin-bottom: 0.75rem; } .error { color: #dc2626; font-size: 0.875rem; margin-bottom: 0.75rem; }
.actions { display: flex; gap: 0.75rem; margin-top: 0.5rem; } .actions { display: flex; gap: 0.75rem; margin-top: 0.5rem; }
.btn-primary { .btn-primary {
flex: 1; padding: 0.75rem; background: #1e40af; color: #fff; flex: 1; padding: 0.75rem; background: #1e40af; color: #fff;
border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600;
@ -372,24 +445,21 @@
} }
.btn-primary:hover:not(:disabled) { background: #1d3a9e; } .btn-primary:hover:not(:disabled) { background: #1d3a9e; }
.btn-primary:disabled { opacity: 0.55; cursor: not-allowed; } .btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-ghost { .btn-ghost {
padding: 0.75rem 1rem; background: none; padding: 0.75rem 1rem; background: none;
border: 1.5px solid #e2e8f0; border-radius: 8px; border: 1.5px solid #e2e8f0; border-radius: 8px;
font-size: 1rem; color: #64748b; cursor: pointer; font-size: 1rem; color: #64748b; cursor: pointer;
} }
.dialog { .dialog {
background: #fff; border-radius: 16px; padding: 1.5rem; background: #fff; border-radius: 16px; padding: 1.5rem;
width: 100%; max-width: 400px; width: 100%; max-width: 400px;
} }
.dialog p { font-size: 1rem; color: #1e293b; margin-bottom: 0.4rem; font-weight: 600; } .dialog p { font-size: 1rem; color: #1e293b; margin-bottom: 0.4rem; font-weight: 600; }
.dialog-sub { font-size: 0.875rem; color: #94a3b8; font-weight: 400 !important; } .dialog-sub { font-weight: 400 !important; font-size: 0.875rem; color: #94a3b8; }
.dialog-actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; } .dialog-actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; }
.btn-danger { .btn-danger {
flex: 1; padding: 0.75rem; background: #dc2626; color: #fff; flex: 1; padding: 0.75rem; background: #dc2626; color: #fff;
border: none; border-radius: 8px; font-size: 0.95rem; font-weight: 600; border: none; border-radius: 8px; font-size: 0.95rem; font-weight: 600; cursor: pointer;
cursor: pointer; transition: background 0.15s;
} }
.btn-danger:hover { background: #b91c1c; } .btn-danger:hover { background: #b91c1c; }
</style> </style>

View file

@ -0,0 +1,23 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const c = app.findCollectionByNameOrId("pbc_2279568741") // termine
c.fields.addAt(99, new Field({
"type": "relation", "id": "relation2001000080", "name": "durchfuehrender_id",
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
"cascadeDelete": false, "collectionId": "_pb_users_auth_", "maxSelect": 1, "minSelect": 0
}))
c.fields.addAt(99, new Field({
"type": "select", "id": "select2001000081", "name": "verfuegbarkeit",
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
"maxSelect": 1, "values": ["offen", "bestaetigt", "abgesagt", "vertretung_gesucht"]
}))
app.save(c)
}, (app) => {
const c = app.findCollectionByNameOrId("pbc_2279568741")
c.fields.removeById("relation2001000080")
c.fields.removeById("select2001000081")
app.save(c)
})