feat: add multi-tenant system with properties, organizations, and public booking

Implement complete multi-property architecture:
- Properties (groups of spaces) with public/private visibility
- Property managers (many-to-many) with role-based permissions
- Organizations with member management
- Anonymous/guest booking support via public API (/api/public/*)
- Property-scoped spaces, bookings, and settings
- Frontend: property selector, organization management, public booking views
- Migration script and updated seed data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-15 00:17:21 +00:00
parent d637513d92
commit e21cf03a16
51 changed files with 6324 additions and 273 deletions

View File

@@ -19,7 +19,21 @@
<span>{{ booking.space?.name || 'Unknown Space' }}</span>
</div>
<div v-if="isAdmin && booking.user" class="detail-row">
<div v-if="booking.is_anonymous && booking.guest_name" class="detail-row">
<UserIcon :size="16" class="detail-icon" />
<span>
{{ booking.guest_name }}
<span class="detail-guest-badge">Guest</span>
<span v-if="booking.guest_email" class="detail-muted">
&middot; {{ booking.guest_email }}
</span>
<span v-if="booking.guest_organization" class="detail-muted">
&middot; {{ booking.guest_organization }}
</span>
</span>
</div>
<div v-else-if="isAdmin && booking.user" class="detail-row">
<UserIcon :size="16" class="detail-icon" />
<span>
{{ booking.user.full_name }}
@@ -127,7 +141,7 @@ const actionButtons = computed<ActionButton[]>(() => {
buttons.push({ key: 'reject', label: 'Reject', icon: XCircle })
}
if (status === 'pending') {
if (status === 'pending' || status === 'approved') {
buttons.push({ key: 'edit', label: 'Edit', icon: Pencil })
}
@@ -273,6 +287,18 @@ onUnmounted(() => {
color: var(--color-text-muted);
}
.detail-guest-badge {
display: inline-block;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
border-radius: 6px;
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
color: var(--color-warning);
vertical-align: middle;
margin-left: 4px;
}
/* Description */
.preview-description {
background: var(--color-bg-secondary);