Files
kmtnflower/src/app/admin/admin.component.html
T

666 lines
24 KiB
HTML

<section class="admin-page">
<div class="container">
<div class="admin-header">
<h1>Admin Dashboard</h1>
<p>Manage shows, events, history, team, and community content</p>
</div>
<!-- Tabs -->
<div class="admin-tabs">
<button class="tab-btn" [class.active]="activeTab() === 'shows'" (click)="activeTab.set('shows')">Shows</button>
<button class="tab-btn" [class.active]="activeTab() === 'events'"
(click)="activeTab.set('events')">Events</button>
<button class="tab-btn" [class.active]="activeTab() === 'history'"
(click)="activeTab.set('history')">History</button>
<button class="tab-btn" [class.active]="activeTab() === 'team'" (click)="activeTab.set('team')">Team</button>
<button class="tab-btn" [class.active]="activeTab() === 'community'"
(click)="activeTab.set('community')">Community</button>
<button class="tab-btn" [class.active]="activeTab() === 'station'"
(click)="activeTab.set('station')">Station</button>
<button class="tab-btn" [class.active]="activeTab() === 'theme'"
(click)="activeTab.set('theme'); onTabChanged()">Theme</button>
<button class="tab-btn" [class.active]="activeTab() === 'underwriters'"
(click)="activeTab.set('underwriters')">Underwriters</button>
<button class="tab-btn" [class.active]="activeTab() === 'mobile-builds'"
(click)="activeTab.set('mobile-builds'); loadThemeJsonStatus()">Mobile Builds</button>
<button class="tab-btn" [class.active]="activeTab() === 'stats'" (click)="activeTab.set('stats')">Stats</button>
</div>
<!-- Shows Tab -->
@if (activeTab() === 'shows') {
@if (shows$ | async; as shows) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Shows ({{ shows.length }})</h2>
<button class="btn btn-add" (click)="openShowForm()">+ Add Show</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Order</th>
<th>Title</th>
<th>Host</th>
<th>Genre</th>
<th>Slots</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (show of shows; track show.id) {
<tr>
<td>{{ show.display_order }}</td>
<td>{{ show.title }}</td>
<td>{{ show.host }}</td>
<td>{{ show.genre }}</td>
<td>
@for (slot of show.schedules; track slot.id) {
<span class="slot-badge">{{ slot.day_label.charAt(0) }} {{ slot.time }}</span>
}
</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editShow(show)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteShow(show.id)">Delete</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="empty-row">No shows yet. Click "Add Show" to create one.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- Events Tab -->
@if (activeTab() === 'events') {
@if (events$ | async; as events) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Events ({{ events.length }})</h2>
<button class="btn btn-add" (click)="openEventForm()">+ Add Event</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Location</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (event of events; track event.id) {
<tr>
<td>{{ event.date }}</td>
<td>{{ event.title }}</td>
<td>{{ event.location }}</td>
<td>{{ event.active ? 'Yes' : 'No' }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editEvent(event)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteEvent(event.id)">Delete</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="empty-row">No events yet. Click "Add Event" to create one.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- History Tab -->
@if (activeTab() === 'history') {
@if (historyEntries$ | async; as entries) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>History Timeline ({{ entries.length }})</h2>
<button class="btn btn-add" (click)="openHistoryForm()">+ Add Entry</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Year</th>
<th>Title</th>
<th>Order</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (entry of entries; track entry.id) {
<tr>
<td>{{ entry.year }}</td>
<td>{{ entry.title }}</td>
<td>{{ entry.display_order }}</td>
<td>{{ entry.active ? 'Yes' : 'No' }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editHistory(entry)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteHistory(entry.id)">Delete</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="empty-row">No history entries yet. Click "Add Entry" to create one.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- Team Tab -->
@if (activeTab() === 'team') {
@if (teamMembers$ | async; as members) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Team Members ({{ members.length }})</h2>
<button class="btn btn-add" (click)="openTeamForm()">+ Add Member</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Name</th>
<th>Title</th>
<th>Order</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (member of members; track member.id) {
<tr>
<td>{{ member.name }}</td>
<td>{{ member.title }}</td>
<td>{{ member.display_order }}</td>
<td>{{ member.active ? 'Yes' : 'No' }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editTeam(member)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteTeam(member.id)">Delete</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="empty-row">No team members yet. Click "Add Member" to create one.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- Community Tab -->
@if (activeTab() === 'community') {
@if (communityHighlights$ | async; as highlights) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Community Highlights ({{ highlights.length }})</h2>
<button class="btn btn-add" (click)="openCommunityForm()">+ Add Highlight</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Icon</th>
<th>Title</th>
<th>Order</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (highlight of highlights; track highlight.id) {
<tr>
<td>{{ highlight.icon }}</td>
<td>{{ highlight.title }}</td>
<td>{{ highlight.display_order }}</td>
<td>{{ highlight.active ? 'Yes' : 'No' }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editCommunity(highlight)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteCommunity(highlight.id)">Delete</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="empty-row">No highlights yet. Click "Add Highlight" to create one.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- Station Tab -->
@if (activeTab() === 'station') {
@if (stationConfig$ | async; as config) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Station Configuration</h2>
<button class="btn btn-add" (click)="openStationForm()">Edit Config</button>
</div>
<div class="station-config-view">
<div class="config-row">
<span class="config-label">Callsign</span>
<span class="config-value">{{ config.callsign }}</span>
</div>
<div class="config-row">
<span class="config-label">Name</span>
<span class="config-value">{{ config.name_primary }} {{ config.name_secondary }}</span>
</div>
<div class="config-row">
<span class="config-label">Tagline</span>
<span class="config-value">{{ config.tagline }}</span>
</div>
<div class="config-row">
<span class="config-label">Frequency</span>
<span class="config-value">{{ config.frequency }}</span>
</div>
<div class="config-row">
<span class="config-label">Founded</span>
<span class="config-value">{{ config.founded_year }}</span>
</div>
<div class="config-row">
<span class="config-label">Nonprofit</span>
<span class="config-value">{{ config.nonprofit_type }} — EIN {{ config.ein }}</span>
</div>
<div class="config-row">
<span class="config-label">Email</span>
<span class="config-value">{{ config.email }}</span>
</div>
<div class="config-row">
<span class="config-label">Phone</span>
<span class="config-value">{{ config.phone }}</span>
</div>
<div class="config-row">
<span class="config-label">Website</span>
<span class="config-value">{{ config.website }}</span>
</div>
<div class="config-row">
<span class="config-label">Donation URL</span>
<span class="config-value">{{ config.donation_url || '(not set)' }}</span>
</div>
<div class="config-row">
<span class="config-label">Play Store URL</span>
<span class="config-value">{{ config.play_store_url || '(not set)' }}</span>
</div>
<div class="config-row">
<span class="config-label">App Store Embed</span>
<span class="config-value">{{ config.app_store_embed_html ? 'Configured' : '(not set)' }}</span>
</div>
</div>
</div>
}
}
<!-- Theme Tab -->
@if (activeTab() === 'theme') {
<div class="tab-content theme-panel">
<div class="tab-toolbar">
<h2>Color Theme</h2>
</div>
<div class="theme-harmony-bar">
<div class="harmony-info">
<span class="harmony-label">Harmony:</span>
<span class="harmony-rule">{{ getHarmonyLabel() }}</span>
</div>
<label class="auto-variants-toggle">
<input type="checkbox" [(ngModel)]="themeAutoVariants">
Auto-generate variants
</label>
</div>
@if (themeSaveSuccess) {
<div class="theme-message theme-success">{{ themeSaveSuccess }}</div>
}
@if (themeSaveError) {
<div class="theme-message theme-error">{{ themeSaveError }}</div>
}
<!-- Color grid -->
<div class="color-grid">
@for (role of colorRoles; track role.field) {
<div class="color-card" [class.has-issue]="hasHarmonyIssue(role.field)">
<div class="color-swatch" [style.background]="themeColors[role.field]">
<input type="color" [value]="themeColors[role.field]"
(input)="onThemeColorChange(role.field, $event.target.value)">
</div>
<div class="color-info">
<span class="color-name">{{ role.label }}</span>
<span class="color-hex">{{ themeColors[role.field] }}</span>
</div>
<div class="color-status">
@if (hasHarmonyIssue(role.field)) {
<span class="harmony-warn" title="Not harmonious — click Auto-Fix to correct">⚠</span>
} @else if (role.group !== 'neutral') {
<span class="harmony-ok">✓</span>
}
</div>
</div>
}
</div>
<div class="theme-actions">
<button type="button" class="btn btn-outline-theme" (click)="resetTheme()">Reset to Defaults</button>
<button type="button" class="btn btn-outline-theme" (click)="autoFixTheme()">Auto-Fix Harmony</button>
<button type="button" class="btn btn-save-theme" (click)="saveTheme()" [disabled]="themeSaving">
{{ themeSaving ? 'Saving…' : 'Save Theme' }}
</button>
</div>
</div>
}
<!-- Underwriters Tab -->
@if (activeTab() === 'underwriters') {
@if (underwriters$ | async; as underwriters) {
<div class="tab-content">
<div class="tab-toolbar">
<h2>Underwriters ({{ underwriters.length }})</h2>
<button class="btn btn-add" (click)="openUnderwriterForm()">+ Add Underwriter</button>
</div>
<table class="admin-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Website</th>
<th>Order</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (uw of underwriters; track uw.id) {
<tr>
<td>{{ uw.name }}</td>
<td class="description-cell">{{ uw.description }}</td>
<td>
@if (uw.website_url) {
<a [href]="uw.website_url" target="_blank" rel="noopener">{{ uw.website_url }}</a>
} @else {
—
}
</td>
<td>{{ uw.display_order }}</td>
<td>{{ uw.active ? 'Yes' : 'No' }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="editUnderwriter(uw)">Edit</button>
<button class="btn-icon btn-delete" (click)="deleteUnderwriter(uw.id)">Delete</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="empty-row">No underwriters yet. Click "Add Underwriter" to create one.</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- Mobile Builds Tab -->
@if (activeTab() === 'mobile-builds') {
<div class="tab-content mobile-builds-panel">
<div class="tab-toolbar">
<h2>Mobile Build Orchestration</h2>
<button class="btn btn-add" (click)="reloadMobileBuilds()">Refresh Requests</button>
</div>
<div class="mobile-build-grid">
<section class="build-card">
<h3>Derived Defaults</h3>
@if (mobileBuildDefaults; as defaults) {
<div class="derived-list">
<div><strong>Tenant:</strong> {{ defaults.tenant_key }}</div>
<div><strong>App Name:</strong> {{ defaults.app_name }}</div>
<div><strong>Support Email:</strong> {{ defaults.support_email }}</div>
<div><strong>Website:</strong> {{ defaults.website }}</div>
<div><strong>Stream URL:</strong> {{ defaults.stream_url || '(not set)' }}</div>
</div>
} @else {
<p class="empty-row">Defaults unavailable. Check station config.</p>
}
</section>
<section class="build-card">
<h3>Theme Export</h3>
@if (themeJsonStatus$ | async; as status) {
@if (status.exists) {
<div class="theme-status-ok">
<div><strong>theme.json</strong> — ready</div>
<div>Path: {{ status.path }}</div>
<div>Generated: {{ status.generated_at }}</div>
<div>Size: {{ status.size }} bytes</div>
</div>
} @else {
<div class="theme-status-missing">
<p><strong>theme.json</strong> not found at {{ status.path }}</p>
<p>Regenerate to create it from the current station config.</p>
</div>
}
<button class="btn btn-outline" (click)="regenerateThemeJson()"
[disabled]="themeJsonLoading$ | async">
{{ (themeJsonLoading$ | async) ? 'Regenerating...' : 'Regenerate' }}
</button>
} @else {
@if (themeJsonLoading$ | async) {
<p class="empty-row">Loading...</p>
}
}
</section>
<section class="build-card">
<h3>Create Build Request</h3>
<div class="form-row platform-row">
<label>
<input type="checkbox" [(ngModel)]="buildAndroid">
Android
</label>
<label>
<input type="checkbox" [(ngModel)]="buildIos">
iOS
</label>
</div>
<div class="form-row">
<label>
Android Application ID
<input type="text" [(ngModel)]="androidApplicationId" placeholder="com.example.radio">
</label>
<label>
iOS Bundle ID
<input type="text" [(ngModel)]="iosBundleId" placeholder="com.example.radio">
</label>
</div>
<div class="form-row">
<label>
Version Name
<input type="text" [(ngModel)]="buildVersionName" placeholder="1.0.0">
</label>
<label>
Build Number
<input type="text" [(ngModel)]="buildNumber" placeholder="1">
</label>
<label>
Release Profile
<input type="text" [(ngModel)]="buildReleaseProfile" placeholder="release">
</label>
</div>
<h4>Required Files</h4>
<div class="form-row">
<label>
Android Keystore (.jks/.keystore)
<input type="file" (change)="onFileChosen($event, 'android-keystore')">
</label>
<label>
Android Descriptor (JSON)
<input type="file" accept=".json,application/json" (change)="onFileChosen($event, 'android-descriptor')">
</label>
</div>
<div class="form-row">
<label>
iOS Certificate (.p12)
<input type="file" accept=".p12" (change)="onFileChosen($event, 'ios-certificate')">
</label>
<label>
iOS Provisioning Profile (.mobileprovision)
<input type="file" accept=".mobileprovision" (change)="onFileChosen($event, 'ios-profile')">
</label>
</div>
@if (mobileBuildStatusMessage) {
<p class="status success">{{ mobileBuildStatusMessage }}</p>
}
@if (mobileBuildErrorMessage) {
<p class="status error">{{ mobileBuildErrorMessage }}</p>
}
<button class="btn btn-add" [disabled]="mobileBuildBusy" (click)="createAndUploadMobileBuildRequest()">
{{ mobileBuildBusy ? 'Working…' : 'Create Draft + Upload Files' }}
</button>
</section>
</div>
<section class="build-card">
<h3>Build Requests</h3>
@if (mobileBuilds$ | async; as builds) {
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Platforms</th>
<th>Version</th>
<th>Status</th>
<th>Preflight</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (build of builds; track build.id) {
<tr [class.selected-build]="selectedMobileBuildId === build.id">
<td>#{{ build.id }}</td>
<td>
{{ build.platform_android ? 'Android' : '' }}
{{ build.platform_android && build.platform_ios ? ' + ' : '' }}
{{ build.platform_ios ? 'iOS' : '' }}
</td>
<td>{{ build.version_name }} ({{ build.build_number }})</td>
<td>{{ build.status }}</td>
<td>{{ build.preflight_passed ? 'Passed' : 'Pending/Failed' }}</td>
<td class="actions">
<button class="btn-icon btn-edit" (click)="onMobileBuildSelected(build.id)">Select</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="empty-row">No build requests yet.</td>
</tr>
}
</tbody>
</table>
}
</section>
@if (selectedMobileBuild; as build) {
<section class="build-card">
<div class="tab-toolbar compact">
<h3>Selected Request #{{ build.id }}</h3>
<div class="action-group">
<button class="btn-icon btn-edit" [disabled]="mobileBuildBusy" (click)="runPreflightForSelectedBuild()">Run
Preflight</button>
<button class="btn-icon btn-edit" [disabled]="mobileBuildBusy || !build.preflight_passed"
(click)="triggerSelectedBuild()">Trigger Build</button>
<button class="btn-icon btn-edit" [disabled]="mobileBuildBusy"
(click)="refreshSelectedBuild()">Refresh</button>
</div>
</div>
@if (mobileBuildPreflightIssues.length > 0) {
<div class="status error">
<strong>Preflight issues:</strong>
<ul>
@for (issue of mobileBuildPreflightIssues; track issue) {
<li>{{ issue }}</li>
}
</ul>
</div>
}
@if (build.error_message) {
<p class="status error"><strong>Error:</strong> {{ build.error_message }}</p>
}
<h4>Artifacts</h4>
<div class="artifact-list">
@for (artifact of build.artifacts; track artifact.id) {
<div class="artifact-row">
<div>
<strong>{{ artifact.file_name }}</strong>
<div class="artifact-meta">{{ artifact.platform }} • {{ artifact.artifact_type }} • {{
artifact.sha256.slice(0, 12) }}…</div>
</div>
<a class="btn-icon btn-edit" [href]="getArtifactDownloadHref(artifact.download_url)">Download</a>
</div>
} @empty {
<p class="empty-row">No artifacts available yet.</p>
}
</div>
<h4>Build Log</h4>
<pre class="build-log">{{ build.build_log || 'No logs yet.' }}</pre>
</section>
}
</div>
}
<!-- Stats Tab -->
@if (activeTab() === 'stats') {
<div class="tab-content">
<app-admin-stats-dashboard />
</div>
}
</div>
<!-- Form modals -->
@if (showShowForm) {
<app-admin-show-form [editItem]="editingShow" (saved)="onShowSaved()" (closed)="onShowClosed()" />
}
@if (showEventForm) {
<app-admin-event-form [editItem]="editingEvent" (saved)="onEventSaved()" (closed)="onEventClosed()" />
}
@if (showStationForm) {
<app-admin-station-form [editItem]="stationConfig$ | async" (saved)="onStationSaved()" (closed)="onStationClosed()" />
}
@if (showHistoryForm) {
<app-admin-history-form [editItem]="editingHistory" (saved)="onHistorySaved()" (closed)="onHistoryClosed()" />
}
@if (showTeamForm) {
<app-admin-team-form [editItem]="editingTeam" (saved)="onTeamSaved()" (closed)="onTeamClosed()" />
}
@if (showCommunityForm) {
<app-admin-community-form [editItem]="editingCommunity" (saved)="onCommunitySaved()" (closed)="onCommunityClosed()" />
}
@if (showUnderwriterForm) {
<app-admin-underwriter-form [editItem]="editingUnderwriter" (saved)="onUnderwriterSaved()"
(closed)="onUnderwriterClosed()" />
}
</section>