Smart-Lock-Gastcodes zu Smart-Lock/Alarmanlagen-Code umgebaut
Statt zeitlich begrenzter Gästecodes, die jeder selbst ausstellen konnte, gibt es jetzt einen dauerhaften Code je Mieter, den ausschließlich der Vermieter vergibt/widerruft. Der Mieter sieht nur seinen eigenen Code (read-only).
This commit is contained in:
parent
f576cddf6a
commit
fdf98a70e5
@ -10,7 +10,7 @@ import { cleaningTasksRouter } from './routes/cleaningTasks';
|
||||
import { expensesRouter } from './routes/expenses';
|
||||
import { inventoryRouter } from './routes/inventory';
|
||||
import { handoverProtocolsRouter } from './routes/handoverProtocols';
|
||||
import { guestCodesRouter } from './routes/guestCodes';
|
||||
import { accessCodesRouter } from './routes/accessCodes';
|
||||
import { storageRouter } from './routes/storage';
|
||||
import { documentsRouter } from './routes/documents';
|
||||
import { contractsRouter } from './routes/contracts';
|
||||
@ -49,7 +49,7 @@ export function createApp() {
|
||||
app.use('/v1', expensesRouter);
|
||||
app.use('/v1', inventoryRouter);
|
||||
app.use('/v1', handoverProtocolsRouter);
|
||||
app.use('/v1', guestCodesRouter);
|
||||
app.use('/v1', accessCodesRouter);
|
||||
app.use('/v1', storageRouter);
|
||||
app.use('/v1', documentsRouter);
|
||||
app.use('/v1', contractsRouter);
|
||||
|
||||
105
backend/src/routes/accessCodes.ts
Normal file
105
backend/src/routes/accessCodes.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { PrismaClient, AccessCodeStatus } from '@prisma/client';
|
||||
import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Router für den Smart-Lock/Alarmanlagen-Code je Mieter.
|
||||
*
|
||||
* Zugriffsregel: nur Vermieter/Admin vergeben oder ändern einen Code für
|
||||
* einen bestimmten Mieter. Der Mieter selbst kann seinen eigenen Code nur
|
||||
* einsehen (lesend), nicht anlegen, ändern oder widerrufen.
|
||||
*/
|
||||
export const accessCodesRouter = Router();
|
||||
|
||||
const DEFAULT_LOCK_DEVICE_ID = 'front-door-01';
|
||||
|
||||
const ACCESS_CODE_SELECT = {
|
||||
id: true,
|
||||
code: true,
|
||||
lockDeviceId: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
user: { select: { id: true, fullName: true } },
|
||||
assignedBy: { select: { id: true, fullName: true } },
|
||||
};
|
||||
|
||||
// POST /access-codes { userId, code, lockDeviceId? } (nur Vermieter/Admin)
|
||||
// Vergibt/ersetzt den Code für den angegebenen Mieter. Ein vorhandener
|
||||
// aktiver Code desselben Mieters wird dabei automatisch widerrufen.
|
||||
accessCodesRouter.post(
|
||||
'/access-codes',
|
||||
requireAuth,
|
||||
requireRole('LANDLORD', 'ADMIN'),
|
||||
async (req: AuthedRequest, res: Response) => {
|
||||
const { userId, code, lockDeviceId } = req.body || {};
|
||||
|
||||
if (!userId || typeof userId !== 'string') {
|
||||
return res.status(400).json({ error: 'userId (Mieter) ist erforderlich' });
|
||||
}
|
||||
if (!code || !String(code).trim()) {
|
||||
return res.status(400).json({ error: 'Code ist erforderlich' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user) {
|
||||
return res.status(400).json({ error: 'Mieter nicht gefunden' });
|
||||
}
|
||||
|
||||
await prisma.accessCode.updateMany({
|
||||
where: { userId, status: AccessCodeStatus.ACTIVE },
|
||||
data: { status: AccessCodeStatus.REVOKED },
|
||||
});
|
||||
|
||||
const accessCode = await prisma.accessCode.create({
|
||||
data: {
|
||||
userId,
|
||||
assignedById: req.user!.id,
|
||||
code: String(code).trim(),
|
||||
lockDeviceId: typeof lockDeviceId === 'string' && lockDeviceId.trim() ? lockDeviceId.trim() : DEFAULT_LOCK_DEVICE_ID,
|
||||
status: AccessCodeStatus.ACTIVE,
|
||||
},
|
||||
select: ACCESS_CODE_SELECT,
|
||||
});
|
||||
|
||||
res.status(201).json({ accessCode });
|
||||
},
|
||||
);
|
||||
|
||||
// GET /access-codes — Vermieter/Admin sehen alle Codes, ein Mieter sieht nur
|
||||
// seinen eigenen (aktiven) Code.
|
||||
accessCodesRouter.get('/access-codes', requireAuth, async (req: AuthedRequest, res: Response) => {
|
||||
const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN';
|
||||
const where: Record<string, unknown> = {};
|
||||
if (!isLandlord) {
|
||||
where.userId = req.user!.id;
|
||||
where.status = AccessCodeStatus.ACTIVE;
|
||||
}
|
||||
|
||||
const accessCodes = await prisma.accessCode.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: ACCESS_CODE_SELECT,
|
||||
});
|
||||
|
||||
res.status(200).json({ accessCodes });
|
||||
});
|
||||
|
||||
// DELETE /access-codes/:id — nur Vermieter/Admin widerrufen einen Code.
|
||||
accessCodesRouter.delete(
|
||||
'/access-codes/:id',
|
||||
requireAuth,
|
||||
requireRole('LANDLORD', 'ADMIN'),
|
||||
async (req: AuthedRequest, res: Response) => {
|
||||
const accessCode = await prisma.accessCode.findUnique({ where: { id: req.params.id } });
|
||||
if (!accessCode) return res.status(404).json({ error: 'Code nicht gefunden' });
|
||||
|
||||
await prisma.accessCode.update({
|
||||
where: { id: req.params.id },
|
||||
data: { status: AccessCodeStatus.REVOKED },
|
||||
});
|
||||
res.status(204).send();
|
||||
},
|
||||
);
|
||||
@ -1,96 +0,0 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { PrismaClient, GuestCodeStatus } from '@prisma/client';
|
||||
import { AuthedRequest, requireAuth } from '../middleware/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Router für Smart-Lock-Gastcodes (Nuki/Tuya). Jeder eingeloggte Nutzer
|
||||
* (Mieter oder Vermieter) kann einen zeitlich begrenzten Gäste-Code
|
||||
* generieren. Ein Mieter sieht/verwaltet nur seine EIGENEN ausgestellten
|
||||
* Codes (wer wann Besuch bekommt, ist privat) — Vermieter/Admin sehen alle.
|
||||
*/
|
||||
export const guestCodesRouter = Router();
|
||||
|
||||
const DEFAULT_LOCK_DEVICE_ID = 'front-door-01';
|
||||
|
||||
function generateCode(): string {
|
||||
return String(Math.floor(100000 + Math.random() * 900000)); // 6-stelliger PIN
|
||||
}
|
||||
|
||||
const GUEST_CODE_SELECT = {
|
||||
id: true,
|
||||
code: true,
|
||||
lockDeviceId: true,
|
||||
validFrom: true,
|
||||
validUntil: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
issuedBy: { select: { id: true, fullName: true } },
|
||||
};
|
||||
|
||||
guestCodesRouter.post('/guest-codes', requireAuth, async (req: AuthedRequest, res: Response) => {
|
||||
const { validFrom, validUntil, lockDeviceId } = req.body || {};
|
||||
|
||||
if (!validFrom || Number.isNaN(Date.parse(validFrom))) {
|
||||
return res.status(400).json({ error: 'validFrom (Datum/Zeit) ist erforderlich' });
|
||||
}
|
||||
if (!validUntil || Number.isNaN(Date.parse(validUntil))) {
|
||||
return res.status(400).json({ error: 'validUntil (Datum/Zeit) ist erforderlich' });
|
||||
}
|
||||
if (new Date(validUntil) <= new Date(validFrom)) {
|
||||
return res.status(400).json({ error: 'validUntil muss nach validFrom liegen' });
|
||||
}
|
||||
|
||||
let code = generateCode();
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
const existing = await prisma.guestCode.findUnique({ where: { code } });
|
||||
if (!existing) break;
|
||||
code = generateCode();
|
||||
}
|
||||
|
||||
const guestCode = await prisma.guestCode.create({
|
||||
data: {
|
||||
issuedById: req.user!.id,
|
||||
code,
|
||||
lockDeviceId: typeof lockDeviceId === 'string' && lockDeviceId.trim() ? lockDeviceId.trim() : DEFAULT_LOCK_DEVICE_ID,
|
||||
validFrom: new Date(validFrom),
|
||||
validUntil: new Date(validUntil),
|
||||
status: GuestCodeStatus.ACTIVE,
|
||||
},
|
||||
select: GUEST_CODE_SELECT,
|
||||
});
|
||||
|
||||
res.status(201).json({ guestCode });
|
||||
});
|
||||
|
||||
guestCodesRouter.get('/guest-codes', requireAuth, async (req: AuthedRequest, res: Response) => {
|
||||
const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN';
|
||||
const where: Record<string, unknown> = {};
|
||||
if (!isLandlord) where.issuedById = req.user!.id;
|
||||
|
||||
const guestCodes = await prisma.guestCode.findMany({
|
||||
where,
|
||||
orderBy: { validFrom: 'desc' },
|
||||
select: GUEST_CODE_SELECT,
|
||||
});
|
||||
|
||||
res.status(200).json({ guestCodes });
|
||||
});
|
||||
|
||||
// Widerrufen — eigener Code oder Vermieter/Admin.
|
||||
guestCodesRouter.delete('/guest-codes/:id', requireAuth, async (req: AuthedRequest, res: Response) => {
|
||||
const guestCode = await prisma.guestCode.findUnique({ where: { id: req.params.id } });
|
||||
if (!guestCode) return res.status(404).json({ error: 'Gästecode nicht gefunden' });
|
||||
|
||||
const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN';
|
||||
if (!isLandlord && guestCode.issuedById !== req.user!.id) {
|
||||
return res.status(403).json({ error: 'Du kannst nur deine eigenen Gästecodes widerrufen' });
|
||||
}
|
||||
|
||||
await prisma.guestCode.update({
|
||||
where: { id: req.params.id },
|
||||
data: { status: GuestCodeStatus.REVOKED },
|
||||
});
|
||||
res.status(204).send();
|
||||
});
|
||||
@ -109,9 +109,8 @@ enum HandoverType {
|
||||
MOVE_OUT
|
||||
}
|
||||
|
||||
enum GuestCodeStatus {
|
||||
enum AccessCodeStatus {
|
||||
ACTIVE
|
||||
EXPIRED
|
||||
REVOKED
|
||||
}
|
||||
|
||||
@ -187,7 +186,8 @@ model User {
|
||||
expensesPaid Expense[]
|
||||
expenseShares ExpenseShare[]
|
||||
handoverProtocols HandoverProtocol[]
|
||||
guestCodesIssued GuestCode[]
|
||||
accessCodeAssigned AccessCode[] @relation("AccessCodeUser")
|
||||
accessCodesGiven AccessCode[] @relation("AccessCodeAssignedBy")
|
||||
notifications Notification[]
|
||||
invitationsSent Invitation[] @relation("InvitationsSent")
|
||||
storageSlots StorageSlot[]
|
||||
@ -540,20 +540,26 @@ model MeterReading {
|
||||
// SMART LOCK / GUEST CODES
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
model GuestCode {
|
||||
id String @id @default(uuid())
|
||||
issuedById String @map("issued_by_id")
|
||||
issuedBy User @relation(fields: [issuedById], references: [id])
|
||||
// Persönlicher Smart-Lock/Alarmanlagen-Code je Mieter. Anders als ein
|
||||
// klassischer zeitlich begrenzter Gästecode ist das ein dauerhafter Code für
|
||||
// den festen Zugang/Alarm-Ein-/Ausschalten des jeweiligen Mieters — daher
|
||||
// nur vom Vermieter/Admin vergebbar, der Mieter kann seinen eigenen Code nur
|
||||
// einsehen (nicht selbst anlegen oder ändern).
|
||||
model AccessCode {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
user User @relation("AccessCodeUser", fields: [userId], references: [id])
|
||||
assignedById String @map("assigned_by_id")
|
||||
assignedBy User @relation("AccessCodeAssignedBy", fields: [assignedById], references: [id])
|
||||
|
||||
code String @unique
|
||||
lockDeviceId String @map("lock_device_id") // Nuki/Tuya Device-ID
|
||||
validFrom DateTime @map("valid_from")
|
||||
validUntil DateTime @map("valid_until")
|
||||
status GuestCodeStatus @default(ACTIVE)
|
||||
code String
|
||||
lockDeviceId String @map("lock_device_id") // Nuki/Tuya/HmIP Device-ID
|
||||
status AccessCodeStatus @default(ACTIVE)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("guest_codes")
|
||||
@@map("access_codes")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@ -926,23 +926,26 @@
|
||||
|
||||
<div class="page" id="page-access">
|
||||
|
||||
<section id="guestCodesSection">
|
||||
<h2>Smart-Lock-Gastcodes</h2>
|
||||
<div class="invite-card" style="margin-bottom:16px;">
|
||||
<div id="guestCodeFormError" style="display:none" class="form-error"></div>
|
||||
<form id="guestCodeForm" class="invite-form-row" style="flex-wrap:wrap;">
|
||||
<section id="accessCodeSection">
|
||||
<h2>Smart-Lock/Alarmanlagen-Code</h2>
|
||||
<div id="accessCodeAssignCard" class="invite-card" style="margin-bottom:16px; display:none">
|
||||
<div id="accessCodeFormError" style="display:none" class="form-error"></div>
|
||||
<form id="accessCodeForm" class="invite-form-row" style="flex-wrap:wrap;">
|
||||
<div class="field" style="flex:1 1 200px;">
|
||||
<label for="guestCodeFrom">Gültig ab</label>
|
||||
<input type="datetime-local" id="guestCodeFrom" required />
|
||||
<label for="accessCodeUser">Mieter</label>
|
||||
<select id="accessCodeUser"></select>
|
||||
</div>
|
||||
<div class="field" style="flex:1 1 200px;">
|
||||
<label for="guestCodeUntil">Gültig bis</label>
|
||||
<input type="datetime-local" id="guestCodeUntil" required />
|
||||
<div class="field" style="flex:1 1 160px;">
|
||||
<label for="accessCodeValue">Code</label>
|
||||
<input type="text" id="accessCodeValue" placeholder="z. B. 4711" maxlength="20" required />
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="width:auto" id="guestCodeSubmitBtn">Code generieren</button>
|
||||
<button type="submit" class="btn-primary" style="width:auto" id="accessCodeSubmitBtn">Code vergeben</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ticket-list" id="guestCodeList"></div>
|
||||
<p id="accessCodeTenantHint" style="color:var(--text-muted); font-size:13px; display:none">
|
||||
Dieser Code wird ausschließlich vom Vermieter vergeben.
|
||||
</p>
|
||||
<div class="ticket-list" id="accessCodeList"></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@ -1136,7 +1139,7 @@
|
||||
const EXPENSE_CATEGORY_LABEL = { HAUSHALT: 'Haushalt', LEBENSMITTEL_GEMEINSAM: 'Lebensmittel (gemeinsam)', REPARATUR_VORAUSLAGE: 'Reparatur-Vorauslage', SONSTIGES: 'Sonstiges' };
|
||||
const ITEM_CONDITION_LABEL = { NEW: 'Neu', GOOD: 'Gut', WEAR_AND_TEAR: 'Abgenutzt', DAMAGED: 'Beschädigt' };
|
||||
const HANDOVER_TYPE_LABEL = { MOVE_IN: 'Einzug', MOVE_OUT: 'Auszug' };
|
||||
const GUEST_CODE_STATUS_LABEL = { ACTIVE: 'Aktiv', EXPIRED: 'Abgelaufen', REVOKED: 'Widerrufen' };
|
||||
const ACCESS_CODE_STATUS_LABEL = { ACTIVE: 'Aktiv', REVOKED: 'Widerrufen' };
|
||||
const STORAGE_LOCATION_LABEL = { FRIDGE: 'Kühlschrank', FREEZER: 'Gefrierfach', KITCHEN_CABINET_1: 'Küchenschrank 1', KITCHEN_CABINET_2: 'Küchenschrank 2', KITCHEN_CABINET_3: 'Küchenschrank 3', PANTRY: 'Vorratsschrank' };
|
||||
const TRASH_TYPE_LABEL = { RESTMUELL: 'Restmüll', BIOMUELL: 'Biomüll', GELBER_SACK: 'Gelber Sack', PAPIER: 'Papier', GLAS: 'Glas' };
|
||||
const TRASH_TYPE_COLOR = { RESTMUELL: '#4A4A4A', BIOMUELL: '#8B5E34', GELBER_SACK: '#E0B400', PAPIER: '#2F80ED', GLAS: '#27AE60' };
|
||||
@ -1393,7 +1396,7 @@
|
||||
initExpenseForm();
|
||||
if (isLandlord) initInventoryForm();
|
||||
if (isLandlord) initHandoverForm();
|
||||
initGuestCodeForm();
|
||||
initAccessCodeForm();
|
||||
if (isLandlord) initStorageForms();
|
||||
if (isLandlord) initDocumentForm();
|
||||
if (isLandlord) initUtilityStatementForm();
|
||||
@ -1407,7 +1410,7 @@
|
||||
loadExpenses();
|
||||
loadInventory();
|
||||
loadHandoverProtocols();
|
||||
loadGuestCodes();
|
||||
loadAccessCodes();
|
||||
loadStorage();
|
||||
loadDocuments();
|
||||
loadNoticeDeadlines();
|
||||
@ -1423,7 +1426,7 @@
|
||||
setInterval(loadExpenses, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadInventory, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadHandoverProtocols, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadGuestCodes, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadAccessCodes, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadStorage, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadDocuments, REFRESH_INTERVAL_MS);
|
||||
setInterval(loadNoticeDeadlines, REFRESH_INTERVAL_MS);
|
||||
@ -2561,49 +2564,69 @@
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// SMART-LOCK-GASTCODES
|
||||
// SMART-LOCK/ALARMANLAGEN-CODE
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
function initGuestCodeForm() {
|
||||
const form = document.getElementById('guestCodeForm');
|
||||
const errorBox = document.getElementById('guestCodeFormError');
|
||||
async function initAccessCodeForm() {
|
||||
const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN';
|
||||
document.getElementById('accessCodeAssignCard').style.display = isLandlord ? 'block' : 'none';
|
||||
document.getElementById('accessCodeTenantHint').style.display = isLandlord ? 'none' : 'block';
|
||||
if (!isLandlord) return;
|
||||
|
||||
const userSelect = document.getElementById('accessCodeUser');
|
||||
userSelect.innerHTML = '';
|
||||
try {
|
||||
const data = await apiFetch('/tenants');
|
||||
data.tenants.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.id;
|
||||
opt.textContent = t.room ? `${t.fullName} (${t.room.roomNumber})` : t.fullName;
|
||||
userSelect.appendChild(opt);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[access-codes] Mieterliste laden fehlgeschlagen:', err.message);
|
||||
}
|
||||
|
||||
const form = document.getElementById('accessCodeForm');
|
||||
const errorBox = document.getElementById('accessCodeFormError');
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
errorBox.style.display = 'none';
|
||||
const submitBtn = document.getElementById('guestCodeSubmitBtn');
|
||||
const submitBtn = document.getElementById('accessCodeSubmitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Generiert…';
|
||||
submitBtn.textContent = 'Vergibt…';
|
||||
try {
|
||||
const validFrom = document.getElementById('guestCodeFrom').value;
|
||||
const validUntil = document.getElementById('guestCodeUntil').value;
|
||||
await apiFetch('/guest-codes', { method: 'POST', body: JSON.stringify({ validFrom, validUntil }) });
|
||||
const userId = userSelect.value;
|
||||
const code = document.getElementById('accessCodeValue').value;
|
||||
await apiFetch('/access-codes', { method: 'POST', body: JSON.stringify({ userId, code }) });
|
||||
form.reset();
|
||||
loadGuestCodes();
|
||||
loadAccessCodes();
|
||||
} catch (err) {
|
||||
errorBox.textContent = err.message;
|
||||
errorBox.style.display = 'block';
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Code generieren';
|
||||
submitBtn.textContent = 'Code vergeben';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadGuestCodes() {
|
||||
async function loadAccessCodes() {
|
||||
if (!currentUser) return;
|
||||
try {
|
||||
const data = await apiFetch('/guest-codes');
|
||||
renderGuestCodes(data.guestCodes);
|
||||
const data = await apiFetch('/access-codes');
|
||||
renderAccessCodes(data.accessCodes);
|
||||
} catch (err) {
|
||||
console.error('[guest-codes] Laden fehlgeschlagen:', err.message);
|
||||
console.error('[access-codes] Laden fehlgeschlagen:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderGuestCodes(codes) {
|
||||
const list = document.getElementById('guestCodeList');
|
||||
function renderAccessCodes(codes) {
|
||||
const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN';
|
||||
const list = document.getElementById('accessCodeList');
|
||||
list.innerHTML = '';
|
||||
if (!codes.length) {
|
||||
list.appendChild(el('div', 'empty-state', 'Noch keine Gästecodes ausgestellt.'));
|
||||
list.appendChild(el('div', 'empty-state', isLandlord ? 'Noch keine Codes vergeben.' : 'Dir wurde noch kein Code zugewiesen.'));
|
||||
return;
|
||||
}
|
||||
codes.forEach(g => {
|
||||
@ -2611,23 +2634,23 @@
|
||||
const left = el('div');
|
||||
left.appendChild(el('div', 'title', `Code: ${g.code}`));
|
||||
left.appendChild(el('div', 'meta-text',
|
||||
`${g.issuedBy.fullName} · ${new Date(g.validFrom).toLocaleString('de-DE')} – ${new Date(g.validUntil).toLocaleString('de-DE')} · ${GUEST_CODE_STATUS_LABEL[g.status] || g.status}`));
|
||||
`${isLandlord && g.user ? g.user.fullName + ' · ' : ''}vergeben von ${g.assignedBy.fullName} · ${ACCESS_CODE_STATUS_LABEL[g.status] || g.status}`));
|
||||
row.appendChild(left);
|
||||
if (g.status === 'ACTIVE') {
|
||||
if (isLandlord && g.status === 'ACTIVE') {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn-secondary';
|
||||
btn.textContent = 'Widerrufen';
|
||||
btn.onclick = () => revokeGuestCode(g.id);
|
||||
btn.onclick = () => revokeAccessCode(g.id);
|
||||
row.appendChild(btn);
|
||||
}
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function revokeGuestCode(id) {
|
||||
async function revokeAccessCode(id) {
|
||||
try {
|
||||
await apiFetch(`/guest-codes/${id}`, { method: 'DELETE' });
|
||||
loadGuestCodes();
|
||||
await apiFetch(`/access-codes/${id}`, { method: 'DELETE' });
|
||||
loadAccessCodes();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user