Simple Managment webapp [LLM]
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user