Durchführender: Verfügbarkeit pro Termin, Umbenennung Trainer→Durchführender
This commit is contained in:
parent
59aa3cbcce
commit
c23ac90d35
4 changed files with 213 additions and 117 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
23
pocketbase/pb_migrations/1779230600_termin_verfuegbarkeit.js
Normal file
23
pocketbase/pb_migrations/1779230600_termin_verfuegbarkeit.js
Normal 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)
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue