Simple Managment webapp [LLM]

This commit is contained in:
2025-12-03 17:20:50 +01:00
parent b521f74951
commit e7f613b5dc
76 changed files with 20009 additions and 1 deletions

View File

@@ -0,0 +1,160 @@
<div class="page-content">
<div class="page-header">
<h2>Subscriptions</h2>
<div class="header-actions">
<button nz-button (click)="loadSubscriptions()">
<span nz-icon nzType="reload"></span>
Refresh
</button>
<button nz-button nzType="primary" (click)="openCreateModal()">
<span nz-icon nzType="plus"></span>
Subscribe
</button>
</div>
</div>
<nz-card>
<nz-tabset (nzSelectedIndexChange)="onTabChange($event)">
<nz-tab nzTitle="All"></nz-tab>
<nz-tab nzTitle="Outgoing"></nz-tab>
<nz-tab nzTitle="Incoming"></nz-tab>
</nz-tabset>
<nz-table
#subscriptionTable
[nzData]="subscriptions()"
[nzLoading]="loading()"
[nzShowPagination]="false"
nzSize="middle"
>
<thead>
<tr>
<th nzWidth="15%">Direction</th>
<th nzWidth="25%">Channel</th>
<th nzWidth="20%">Subscriber / Owner</th>
<th nzWidth="15%">Status</th>
<th nzWidth="15%">Created</th>
<th nzWidth="10%">Actions</th>
</tr>
</thead>
<tbody>
@for (sub of subscriptions(); track sub.subscription_id) {
<tr>
<td>
<nz-tag [nzColor]="isOutgoing(sub) ? 'blue' : 'purple'">
{{ getDirectionLabel(sub) }}
</nz-tag>
</td>
<td>
<span class="mono">{{ sub.channel_internal_name }}</span>
</td>
<td>
@if (isOutgoing(sub)) {
<span class="label">Owner:</span>
<span class="mono">{{ sub.channel_owner_user_id }}</span>
} @else {
<span class="label">Subscriber:</span>
<span class="mono">{{ sub.subscriber_user_id }}</span>
}
</td>
<td>
<nz-tag [nzColor]="getStatusInfo(sub).color">
{{ getStatusInfo(sub).label }}
</nz-tag>
</td>
<td>
<span nz-tooltip [nzTooltipTitle]="sub.timestamp_created">
{{ sub.timestamp_created | relativeTime }}
</span>
</td>
<td>
<div class="action-buttons">
@if (!sub.confirmed && isOwner(sub)) {
<!-- Incoming unconfirmed: can accept or deny -->
<button
nz-button
nzSize="small"
nzType="primary"
nz-tooltip
nzTooltipTitle="Accept"
(click)="acceptSubscription(sub)"
>
<span nz-icon nzType="check"></span>
</button>
<button
nz-button
nzSize="small"
nzDanger
nz-tooltip
nzTooltipTitle="Deny"
nz-popconfirm
nzPopconfirmTitle="Deny this subscription request?"
(nzOnConfirm)="denySubscription(sub)"
>
<span nz-icon nzType="close"></span>
</button>
} @else {
<!-- Confirmed or outgoing: can revoke -->
<button
nz-button
nzSize="small"
nzDanger
nz-tooltip
nzTooltipTitle="Revoke"
nz-popconfirm
nzPopconfirmTitle="Revoke this subscription?"
(nzOnConfirm)="revokeSubscription(sub)"
>
<span nz-icon nzType="delete"></span>
</button>
}
</div>
</td>
</tr>
} @empty {
<tr>
<td colspan="6">
<nz-empty nzNotFoundContent="No subscriptions found"></nz-empty>
</td>
</tr>
}
</tbody>
</nz-table>
</nz-card>
</div>
<!-- Create Subscription Modal -->
<nz-modal
[(nzVisible)]="showCreateModal"
nzTitle="Subscribe to Channel"
(nzOnCancel)="closeCreateModal()"
(nzOnOk)="createSubscription()"
[nzOkLoading]="creating()"
[nzOkDisabled]="!newChannelOwner.trim() || !newChannelName.trim()"
>
<ng-container *nzModalContent>
<p class="modal-hint">Enter the channel owner's User ID and the channel name to subscribe.</p>
<nz-form-item>
<nz-form-label>Channel Owner User ID</nz-form-label>
<nz-form-control>
<input
type="text"
nz-input
placeholder="e.g., USR12345"
[(ngModel)]="newChannelOwner"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item class="mb-0">
<nz-form-label>Channel Name</nz-form-label>
<nz-form-control>
<input
type="text"
nz-input
placeholder="e.g., main"
[(ngModel)]="newChannelName"
/>
</nz-form-control>
</nz-form-item>
</ng-container>
</nz-modal>

View File

@@ -0,0 +1,32 @@
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h2 {
margin: 0;
}
}
.header-actions {
display: flex;
gap: 8px;
}
.label {
font-size: 12px;
color: #999;
margin-right: 4px;
}
.action-buttons {
display: flex;
gap: 4px;
}
.modal-hint {
color: #666;
font-size: 13px;
margin-bottom: 16px;
}

View File

@@ -0,0 +1,186 @@
import { Component, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzTabsModule } from 'ng-zorro-antd/tabs';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service';
import { NotificationService } from '../../../core/services/notification.service';
import { Subscription, SubscriptionFilter } from '../../../core/models';
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
type TabDirection = 'both' | 'outgoing' | 'incoming';
@Component({
selector: 'app-subscription-list',
standalone: true,
imports: [
CommonModule,
FormsModule,
NzTableModule,
NzButtonModule,
NzIconModule,
NzTagModule,
NzEmptyModule,
NzCardModule,
NzTabsModule,
NzPopconfirmModule,
NzModalModule,
NzFormModule,
NzInputModule,
NzToolTipModule,
RelativeTimePipe,
],
templateUrl: './subscription-list.component.html',
styleUrl: './subscription-list.component.scss'
})
export class SubscriptionListComponent implements OnInit {
private apiService = inject(ApiService);
private authService = inject(AuthService);
private notification = inject(NotificationService);
subscriptions = signal<Subscription[]>([]);
loading = signal(false);
direction: TabDirection = 'both';
// Create subscription modal
showCreateModal = signal(false);
newChannelOwner = '';
newChannelName = '';
creating = signal(false);
ngOnInit(): void {
this.loadSubscriptions();
}
loadSubscriptions(): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.loading.set(true);
const filter: SubscriptionFilter = {};
if (this.direction !== 'both') {
filter.direction = this.direction;
}
this.apiService.getSubscriptions(userId, filter).subscribe({
next: (response) => {
this.subscriptions.set(response.subscriptions);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
}
});
}
onTabChange(index: number): void {
const directions: TabDirection[] = ['both', 'outgoing', 'incoming'];
this.direction = directions[index];
this.loadSubscriptions();
}
isOutgoing(sub: Subscription): boolean {
const userId = this.authService.getUserId();
return sub.subscriber_user_id === userId;
}
isOwner(sub: Subscription): boolean {
const userId = this.authService.getUserId();
return sub.channel_owner_user_id === userId;
}
// Actions
acceptSubscription(sub: Subscription): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.confirmSubscription(userId, sub.subscription_id, { confirmed: true }).subscribe({
next: () => {
this.notification.success('Subscription accepted');
this.loadSubscriptions();
}
});
}
denySubscription(sub: Subscription): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.deleteSubscription(userId, sub.subscription_id).subscribe({
next: () => {
this.notification.success('Subscription denied');
this.loadSubscriptions();
}
});
}
revokeSubscription(sub: Subscription): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.deleteSubscription(userId, sub.subscription_id).subscribe({
next: () => {
this.notification.success('Subscription revoked');
this.loadSubscriptions();
}
});
}
// Create subscription
openCreateModal(): void {
this.newChannelOwner = '';
this.newChannelName = '';
this.showCreateModal.set(true);
}
closeCreateModal(): void {
this.showCreateModal.set(false);
}
createSubscription(): void {
const userId = this.authService.getUserId();
if (!userId || !this.newChannelOwner.trim() || !this.newChannelName.trim()) return;
this.creating.set(true);
this.apiService.createSubscription(userId, {
channel_owner_user_id: this.newChannelOwner.trim(),
channel_internal_name: this.newChannelName.trim()
}).subscribe({
next: () => {
this.notification.success('Subscription request sent');
this.closeCreateModal();
this.creating.set(false);
this.loadSubscriptions();
},
error: () => {
this.creating.set(false);
}
});
}
getStatusInfo(sub: Subscription): { label: string; color: string } {
if (sub.confirmed) {
return { label: 'Confirmed', color: 'green' };
}
return { label: 'Pending', color: 'orange' };
}
getDirectionLabel(sub: Subscription): string {
if (this.isOutgoing(sub)) {
return 'Outgoing';
}
return 'Incoming';
}
}