Feature: Gasthund-Zugang für Sitter
- sitting_subscriptions Tabelle (dog_id, owner_id, sitter_id, valid_until) - POST/DELETE/GET /api/sitting-access — Zugang gewähren/widerrufen/auflisten - GET /api/dogs gibt Gasthunde zurück (is_guest=True, sitting_until, owner_name) - Diary POST erlaubt Sitter-Schreibzugang; PATCH/DELETE nur für Besitzer - Dog-Switcher: GAST-Badge bei fremden Hunden - Dog-Profil: Sitter-Zugang-Sektion (nur für Besitzer), Freund auswählen + Datum - Diary Detail-View: Bearbeiten-Button für Gasthunde ausgeblendet
This commit is contained in:
parent
eef787cc72
commit
289158b2cd
10 changed files with 327 additions and 18 deletions
|
|
@ -452,7 +452,7 @@ window.Page_diary = (() => {
|
|||
: ''}
|
||||
${dogsHtml}
|
||||
${photo}
|
||||
<button class="btn btn-secondary" style="width:100%;margin-top:var(--space-4)" id="detail-edit">Bearbeiten</button>
|
||||
${!_appState?.activeDog?.is_guest ? `<button class="btn btn-secondary" style="width:100%;margin-top:var(--space-4)" id="detail-edit">Bearbeiten</button>` : ''}
|
||||
`;
|
||||
|
||||
UI.modal.open({ title: entry.titel || typ.label, body });
|
||||
|
|
|
|||
|
|
@ -177,31 +177,48 @@ window.Page_dog_profile = (() => {
|
|||
` : ''}
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||||
<button class="btn btn-primary w-full" id="dp-edit-btn">
|
||||
${!dog.is_guest ? `<button class="btn btn-primary w-full" id="dp-edit-btn">
|
||||
Profil bearbeiten
|
||||
</button>
|
||||
</button>` : ''}
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
<button class="btn btn-secondary" style="flex:1" id="dp-ausweis-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
|
||||
Ausweis
|
||||
</button>
|
||||
<button class="btn btn-secondary" style="flex:1" id="dp-share-btn">
|
||||
${!dog.is_guest ? `<button class="btn btn-secondary" style="flex:1" id="dp-share-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#share-network"></use></svg>
|
||||
Teilen
|
||||
</button>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-secondary w-full" id="dp-add-dog-btn">
|
||||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-add-dog-btn">
|
||||
+ Weiteren Hund anlegen
|
||||
</button>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
${dog.user_id === _appState.user?.id ? `
|
||||
<div class="card" style="margin-bottom:var(--space-5)">
|
||||
<div style="padding:var(--space-4);border-bottom:1px solid var(--c-border)">
|
||||
<div style="font-weight:600">Sitter-Zugang</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
Gib einem Freund temporären Schreibzugang für diesen Hund
|
||||
</div>
|
||||
</div>
|
||||
<div id="dp-sitting-access" style="padding:var(--space-4)">Lade…</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
// Foto-Editor öffnen
|
||||
document.getElementById('dp-photo-edit-btn')?.addEventListener('click', () => {
|
||||
_showPhotoEditor(dog);
|
||||
});
|
||||
|
||||
// Sitter-Zugang laden (nur für Besitzer)
|
||||
if (dog.user_id === _appState.user?.id) {
|
||||
_loadSittingAccess(dog.id);
|
||||
}
|
||||
// NFC-Link kopieren
|
||||
document.getElementById('dp-copy-link-btn')?.addEventListener('click', async () => {
|
||||
const url = `https://banyaro.app/hund/${dog.id}`;
|
||||
|
|
@ -238,6 +255,113 @@ window.Page_dog_profile = (() => {
|
|||
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// SITTER-ZUGANG
|
||||
// ----------------------------------------------------------
|
||||
async function _loadSittingAccess(dogId) {
|
||||
const wrap = document.getElementById('dp-sitting-access');
|
||||
if (!wrap) return;
|
||||
|
||||
try {
|
||||
const [accessData, friendsData] = await Promise.all([
|
||||
API.sittingAccess.my(),
|
||||
API.friends.list(),
|
||||
]);
|
||||
|
||||
const active = (accessData.as_owner || []).filter(s => s.dog_id === dogId);
|
||||
const friends = (friendsData?.friends || []);
|
||||
|
||||
let activeHtml = '';
|
||||
if (active.length) {
|
||||
activeHtml = active.map(s => `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||||
padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
|
||||
border-radius:var(--radius-md);margin-bottom:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg>
|
||||
<div style="flex:1;font-size:var(--text-sm)">
|
||||
<strong>${_esc(s.sitter_name)}</strong>
|
||||
<span style="color:var(--c-text-muted)"> · bis ${_esc(s.valid_until)}</span>
|
||||
</div>
|
||||
<button class="btn btn-link btn-sm sa-revoke-btn" data-sub-id="${s.id}"
|
||||
style="color:var(--c-danger);padding:0">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
const friendOptions = friends.length
|
||||
? friends.map(f => `<option value="${f.friend_id}">${_esc(f.friend_name)}</option>`).join('')
|
||||
: '<option value="" disabled>Keine Freunde vorhanden</option>';
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const defaultUntil = new Date(Date.now() + 14 * 86400000).toISOString().slice(0, 10);
|
||||
|
||||
wrap.innerHTML = `
|
||||
${activeHtml}
|
||||
${friends.length ? `
|
||||
<div style="margin-top:var(--space-3)">
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
||||
margin-bottom:var(--space-2);font-weight:600">Zugang gewähren</div>
|
||||
<div style="display:grid;grid-template-columns:1fr auto;gap:var(--space-2);
|
||||
align-items:end">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Freund</label>
|
||||
<select class="form-control form-control-sm" id="sa-friend-select">
|
||||
<option value="">Freund wählen…</option>
|
||||
${friendOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Gültig bis</label>
|
||||
<input class="form-control form-control-sm" type="date" id="sa-until-input"
|
||||
value="${defaultUntil}" min="${today}">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm w-full" id="sa-grant-btn"
|
||||
style="margin-top:var(--space-2)">
|
||||
Zugang gewähren
|
||||
</button>
|
||||
</div>
|
||||
` : `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0">
|
||||
Füge zuerst Freunde hinzu, um ihnen Zugang zu gewähren.
|
||||
</p>`}
|
||||
`;
|
||||
|
||||
// Event-Listener
|
||||
wrap.querySelectorAll('.sa-revoke-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const subId = parseInt(btn.dataset.subId);
|
||||
try {
|
||||
await API.sittingAccess.revoke(subId);
|
||||
_loadSittingAccess(dogId);
|
||||
} catch (e) {
|
||||
UI.toast.error(e.message || 'Fehler beim Widerrufen.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('sa-grant-btn')?.addEventListener('click', async () => {
|
||||
const sitterId = parseInt(document.getElementById('sa-friend-select').value);
|
||||
const validUntil = document.getElementById('sa-until-input').value;
|
||||
if (!sitterId) { UI.toast.warning('Bitte einen Freund auswählen.'); return; }
|
||||
if (!validUntil) { UI.toast.warning('Bitte ein Datum angeben.'); return; }
|
||||
const btn = document.getElementById('sa-grant-btn');
|
||||
UI.setLoading(btn, true);
|
||||
try {
|
||||
await API.sittingAccess.grant({ dog_id: dogId, sitter_id: sitterId, valid_until: validUntil });
|
||||
UI.toast.success('Sitter-Zugang gewährt.');
|
||||
_loadSittingAccess(dogId);
|
||||
} catch (e) {
|
||||
UI.setLoading(btn, false);
|
||||
UI.toast.error(e.message || 'Fehler beim Gewähren.');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
if (wrap) wrap.innerHTML = '<p style="color:var(--c-danger);font-size:var(--text-sm)">Fehler beim Laden.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function _showChipEdit(dog) {
|
||||
UI.modal.open({
|
||||
title: 'Transpondernummer',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue