Simple Managment webapp [LLM]
This commit is contained in:
@@ -18,10 +18,10 @@ export interface Message {
|
||||
export interface MessageListParams {
|
||||
after?: string;
|
||||
before?: string;
|
||||
channel?: string;
|
||||
priority?: number;
|
||||
channel_id?: string[];
|
||||
priority?: number[];
|
||||
search?: string;
|
||||
sender?: string;
|
||||
sender?: string[];
|
||||
subscription_status?: 'all' | 'confirmed' | 'unconfirmed';
|
||||
trimmed?: boolean;
|
||||
page_size?: number;
|
||||
|
||||
@@ -135,10 +135,22 @@ export class ApiService {
|
||||
if (params) {
|
||||
if (params.after) httpParams = httpParams.set('after', params.after);
|
||||
if (params.before) httpParams = httpParams.set('before', params.before);
|
||||
if (params.channel) httpParams = httpParams.set('channel', params.channel);
|
||||
if (params.priority !== undefined) httpParams = httpParams.set('priority', params.priority);
|
||||
if (params.channel_id) {
|
||||
for (const c of params.channel_id) {
|
||||
httpParams = httpParams.append('channel_id', c);
|
||||
}
|
||||
}
|
||||
if (params.priority) {
|
||||
for (const p of params.priority) {
|
||||
httpParams = httpParams.append('priority', p);
|
||||
}
|
||||
}
|
||||
if (params.search) httpParams = httpParams.set('search', params.search);
|
||||
if (params.sender) httpParams = httpParams.set('sender', params.sender);
|
||||
if (params.sender) {
|
||||
for (const s of params.sender) {
|
||||
httpParams = httpParams.append('sender', s);
|
||||
}
|
||||
}
|
||||
if (params.subscription_status) httpParams = httpParams.set('subscription_status', params.subscription_status);
|
||||
if (params.trimmed !== undefined) httpParams = httpParams.set('trimmed', params.trimmed);
|
||||
if (params.page_size) httpParams = httpParams.set('page_size', params.page_size);
|
||||
|
||||
88
webapp/src/app/core/services/channel-cache.service.ts
Normal file
88
webapp/src/app/core/services/channel-cache.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, of, map, shareReplay, catchError } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import { ChannelWithSubscription } from '../models';
|
||||
|
||||
export interface ResolvedChannel {
|
||||
channelId: string;
|
||||
displayName: string;
|
||||
internalName: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChannelCacheService {
|
||||
private apiService = inject(ApiService);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
private channelsCache$: Observable<Map<string, ChannelWithSubscription>> | null = null;
|
||||
|
||||
getAllChannels(): Observable<ChannelWithSubscription[]> {
|
||||
const userId = this.authService.getUserId();
|
||||
if (!userId) {
|
||||
return of([]);
|
||||
}
|
||||
return this.apiService.getChannels(userId, 'owned').pipe(
|
||||
map(response => response.channels),
|
||||
catchError(() => of([]))
|
||||
);
|
||||
}
|
||||
|
||||
resolveChannel(channelId: string): Observable<ResolvedChannel> {
|
||||
return this.getChannelsMap().pipe(
|
||||
map(channelsMap => {
|
||||
const channel = channelsMap.get(channelId);
|
||||
return {
|
||||
channelId,
|
||||
displayName: channel?.display_name || channel?.internal_name || channelId,
|
||||
internalName: channel?.internal_name || channelId
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
resolveChannels(channelIds: string[]): Observable<Map<string, ResolvedChannel>> {
|
||||
return this.getChannelsMap().pipe(
|
||||
map(channelsMap => {
|
||||
const resolved = new Map<string, ResolvedChannel>();
|
||||
for (const channelId of channelIds) {
|
||||
const channel = channelsMap.get(channelId);
|
||||
resolved.set(channelId, {
|
||||
channelId,
|
||||
displayName: channel?.display_name || channel?.internal_name || channelId,
|
||||
internalName: channel?.internal_name || channelId
|
||||
});
|
||||
}
|
||||
return resolved;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getChannelsMap(): Observable<Map<string, ChannelWithSubscription>> {
|
||||
const userId = this.authService.getUserId();
|
||||
if (!userId) {
|
||||
return of(new Map());
|
||||
}
|
||||
|
||||
if (!this.channelsCache$) {
|
||||
this.channelsCache$ = this.apiService.getChannels(userId, 'owned').pipe(
|
||||
map(response => {
|
||||
const map = new Map<string, ChannelWithSubscription>();
|
||||
for (const channel of response.channels) {
|
||||
map.set(channel.channel_id, channel);
|
||||
}
|
||||
return map;
|
||||
}),
|
||||
catchError(() => of(new Map())),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
return this.channelsCache$;
|
||||
}
|
||||
|
||||
clearCache(): void {
|
||||
this.channelsCache$ = null;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>You need an admin key to access the dashboard.</p>
|
||||
<p>You need an admin key to access.</p>
|
||||
</div>
|
||||
</nz-card>
|
||||
</div>
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
<nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels">
|
||||
All Channels
|
||||
</nz-tag>
|
||||
} @else if (key.channels && key.channels.length > 0) {
|
||||
@for (channelId of key.channels; track channelId) {
|
||||
<nz-tag nzColor="orange" nz-tooltip [nzTooltipTitle]="channelId">
|
||||
{{ getChannelDisplayName(channelId) }}
|
||||
</nz-tag>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
@@ -73,22 +79,29 @@
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!isCurrentKey(key)) {
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
nz-button
|
||||
nzSize="small"
|
||||
nzDanger
|
||||
nz-popconfirm
|
||||
nzPopconfirmTitle="Are you sure you want to delete this key?"
|
||||
(nzOnConfirm)="deleteKey(key)"
|
||||
nz-tooltip
|
||||
nzTooltipTitle="Edit key"
|
||||
(click)="openEditModal(key)"
|
||||
>
|
||||
<span nz-icon nzType="delete"></span>
|
||||
<span nz-icon nzType="edit"></span>
|
||||
</button>
|
||||
} @else {
|
||||
<span class="text-muted" nz-tooltip nzTooltipTitle="Cannot delete the key you're currently using">
|
||||
-
|
||||
</span>
|
||||
}
|
||||
@if (!isCurrentKey(key)) {
|
||||
<button
|
||||
nz-button
|
||||
nzSize="small"
|
||||
nzDanger
|
||||
nz-popconfirm
|
||||
nzPopconfirmTitle="Are you sure you want to delete this key?"
|
||||
(nzOnConfirm)="deleteKey(key)"
|
||||
>
|
||||
<span nz-icon nzType="delete"></span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
@@ -180,11 +193,33 @@
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
|
||||
<nz-form-item class="mb-0">
|
||||
<nz-form-item>
|
||||
<label nz-checkbox [(ngModel)]="newKeyAllChannels">
|
||||
Access to all channels
|
||||
</label>
|
||||
</nz-form-item>
|
||||
|
||||
@if (!newKeyAllChannels) {
|
||||
<nz-form-item class="mb-0">
|
||||
<nz-form-label>Channels</nz-form-label>
|
||||
<nz-form-control>
|
||||
<nz-select
|
||||
[(ngModel)]="newKeyChannels"
|
||||
nzMode="multiple"
|
||||
nzPlaceHolder="Select channels"
|
||||
nzShowSearch
|
||||
style="width: 100%"
|
||||
>
|
||||
@for (channel of availableChannels(); track channel.channel_id) {
|
||||
<nz-option
|
||||
[nzValue]="channel.channel_id"
|
||||
[nzLabel]="getChannelLabel(channel)"
|
||||
></nz-option>
|
||||
}
|
||||
</nz-select>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
</nz-modal>
|
||||
@@ -205,3 +240,87 @@
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<!-- Edit Key Modal -->
|
||||
<nz-modal
|
||||
[(nzVisible)]="showEditModal"
|
||||
nzTitle="Edit Key"
|
||||
(nzOnCancel)="closeEditModal()"
|
||||
[nzFooter]="editModalFooter"
|
||||
nzWidth="500px"
|
||||
>
|
||||
<ng-container *nzModalContent>
|
||||
<nz-form-item>
|
||||
<nz-form-label>Name</nz-form-label>
|
||||
<nz-form-control>
|
||||
<input
|
||||
type="text"
|
||||
nz-input
|
||||
placeholder="Enter a name for this key"
|
||||
[(ngModel)]="editKeyName"
|
||||
/>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
|
||||
<nz-form-item>
|
||||
<nz-form-label>Permissions</nz-form-label>
|
||||
<nz-form-control>
|
||||
<div class="permission-checkboxes">
|
||||
@for (opt of permissionOptions; track opt.value) {
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="isEditPermissionChecked(opt.value)"
|
||||
[nzDisabled]="opt.value !== 'A' && isEditPermissionChecked('A')"
|
||||
(nzCheckedChange)="onEditPermissionChange(opt.value, $event)"
|
||||
>
|
||||
<nz-tag [nzColor]="getPermissionColor(opt.value)">{{ opt.value }}</nz-tag>
|
||||
<span class="perm-label">{{ opt.label }}</span>
|
||||
<span class="perm-desc">- {{ opt.description }}</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
|
||||
<nz-form-item>
|
||||
<label nz-checkbox [(ngModel)]="editKeyAllChannels">
|
||||
Access to all channels
|
||||
</label>
|
||||
</nz-form-item>
|
||||
|
||||
@if (!editKeyAllChannels) {
|
||||
<nz-form-item class="mb-0">
|
||||
<nz-form-label>Channels</nz-form-label>
|
||||
<nz-form-control>
|
||||
<nz-select
|
||||
[(ngModel)]="editKeyChannels"
|
||||
nzMode="multiple"
|
||||
nzPlaceHolder="Select channels"
|
||||
nzShowSearch
|
||||
style="width: 100%"
|
||||
>
|
||||
@for (channel of availableChannels(); track channel.channel_id) {
|
||||
<nz-option
|
||||
[nzValue]="channel.channel_id"
|
||||
[nzLabel]="getChannelLabel(channel)"
|
||||
></nz-option>
|
||||
}
|
||||
</nz-select>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
}
|
||||
</ng-container>
|
||||
</nz-modal>
|
||||
|
||||
<ng-template #editModalFooter>
|
||||
<button nz-button (click)="closeEditModal()">Cancel</button>
|
||||
<button
|
||||
nz-button
|
||||
nzType="primary"
|
||||
[nzLoading]="updating()"
|
||||
[disabled]="!editKeyName.trim() || editKeyPermissions.length === 0"
|
||||
(click)="updateKey()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
|
||||
@@ -14,10 +14,12 @@ import { NzInputModule } from 'ng-zorro-antd/input';
|
||||
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
|
||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||
import { NzAlertModule } from 'ng-zorro-antd/alert';
|
||||
import { NzSelectModule } from 'ng-zorro-antd/select';
|
||||
import { ApiService } from '../../../core/services/api.service';
|
||||
import { AuthService } from '../../../core/services/auth.service';
|
||||
import { NotificationService } from '../../../core/services/notification.service';
|
||||
import { KeyToken, parsePermissions, TokenPermission } from '../../../core/models';
|
||||
import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service';
|
||||
import { KeyToken, parsePermissions, TokenPermission, ChannelWithSubscription } from '../../../core/models';
|
||||
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
|
||||
|
||||
@@ -46,6 +48,7 @@ interface PermissionOption {
|
||||
NzCheckboxModule,
|
||||
NzToolTipModule,
|
||||
NzAlertModule,
|
||||
NzSelectModule,
|
||||
RelativeTimePipe,
|
||||
CopyToClipboardDirective,
|
||||
],
|
||||
@@ -56,19 +59,32 @@ export class KeyListComponent implements OnInit {
|
||||
private apiService = inject(ApiService);
|
||||
private authService = inject(AuthService);
|
||||
private notification = inject(NotificationService);
|
||||
private channelCacheService = inject(ChannelCacheService);
|
||||
|
||||
keys = signal<KeyToken[]>([]);
|
||||
currentKeyId = signal<string | null>(null);
|
||||
loading = signal(false);
|
||||
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
|
||||
availableChannels = signal<ChannelWithSubscription[]>([]);
|
||||
|
||||
// Create modal
|
||||
showCreateModal = signal(false);
|
||||
newKeyName = '';
|
||||
newKeyPermissions: TokenPermission[] = ['CR'];
|
||||
newKeyAllChannels = true;
|
||||
newKeyChannels: string[] = [];
|
||||
creating = signal(false);
|
||||
createdKey = signal<KeyToken | null>(null);
|
||||
|
||||
// Edit modal
|
||||
showEditModal = signal(false);
|
||||
editingKey = signal<KeyToken | null>(null);
|
||||
editKeyName = '';
|
||||
editKeyPermissions: TokenPermission[] = [];
|
||||
editKeyAllChannels = true;
|
||||
editKeyChannels: string[] = [];
|
||||
updating = signal(false);
|
||||
|
||||
permissionOptions: PermissionOption[] = [
|
||||
{ value: 'A', label: 'Admin', description: 'Full access to all operations' },
|
||||
{ value: 'CR', label: 'Channel Read', description: 'Read messages from channels' },
|
||||
@@ -79,6 +95,17 @@ export class KeyListComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.loadKeys();
|
||||
this.loadCurrentKey();
|
||||
this.loadAvailableChannels();
|
||||
}
|
||||
|
||||
loadAvailableChannels(): void {
|
||||
this.channelCacheService.getAllChannels().subscribe(channels => {
|
||||
this.availableChannels.set(channels);
|
||||
});
|
||||
}
|
||||
|
||||
getChannelLabel(channel: ChannelWithSubscription): string {
|
||||
return channel.display_name || channel.internal_name;
|
||||
}
|
||||
|
||||
loadKeys(): void {
|
||||
@@ -90,6 +117,7 @@ export class KeyListComponent implements OnInit {
|
||||
next: (response) => {
|
||||
this.keys.set(response.keys);
|
||||
this.loading.set(false);
|
||||
this.resolveChannelNames(response.keys);
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
@@ -97,6 +125,28 @@ export class KeyListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private resolveChannelNames(keys: KeyToken[]): void {
|
||||
const allChannelIds = new Set<string>();
|
||||
for (const key of keys) {
|
||||
if (!key.all_channels && key.channels) {
|
||||
for (const channelId of key.channels) {
|
||||
allChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allChannelIds.size > 0) {
|
||||
this.channelCacheService.resolveChannels([...allChannelIds]).subscribe(resolved => {
|
||||
this.channelNames.set(resolved);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getChannelDisplayName(channelId: string): string {
|
||||
const resolved = this.channelNames().get(channelId);
|
||||
return resolved?.displayName || channelId;
|
||||
}
|
||||
|
||||
loadCurrentKey(): void {
|
||||
const userId = this.authService.getUserId();
|
||||
if (!userId) return;
|
||||
@@ -134,6 +184,7 @@ export class KeyListComponent implements OnInit {
|
||||
this.newKeyName = '';
|
||||
this.newKeyPermissions = ['CR'];
|
||||
this.newKeyAllChannels = true;
|
||||
this.newKeyChannels = [];
|
||||
this.createdKey.set(null);
|
||||
this.showCreateModal.set(true);
|
||||
}
|
||||
@@ -150,7 +201,8 @@ export class KeyListComponent implements OnInit {
|
||||
this.apiService.createKey(userId, {
|
||||
name: this.newKeyName.trim(),
|
||||
permissions: this.newKeyPermissions.join(';'),
|
||||
all_channels: this.newKeyAllChannels
|
||||
all_channels: this.newKeyAllChannels,
|
||||
channels: this.newKeyAllChannels ? undefined : this.newKeyChannels
|
||||
}).subscribe({
|
||||
next: (key) => {
|
||||
this.createdKey.set(key);
|
||||
@@ -198,4 +250,59 @@ export class KeyListComponent implements OnInit {
|
||||
isPermissionChecked(perm: TokenPermission): boolean {
|
||||
return this.newKeyPermissions.includes(perm);
|
||||
}
|
||||
|
||||
// Edit key modal
|
||||
openEditModal(key: KeyToken): void {
|
||||
this.editingKey.set(key);
|
||||
this.editKeyName = key.name;
|
||||
this.editKeyPermissions = parsePermissions(key.permissions);
|
||||
this.editKeyAllChannels = key.all_channels;
|
||||
this.editKeyChannels = key.channels ? [...key.channels] : [];
|
||||
this.showEditModal.set(true);
|
||||
}
|
||||
|
||||
closeEditModal(): void {
|
||||
this.showEditModal.set(false);
|
||||
this.editingKey.set(null);
|
||||
}
|
||||
|
||||
updateKey(): void {
|
||||
const userId = this.authService.getUserId();
|
||||
const key = this.editingKey();
|
||||
if (!userId || !key || !this.editKeyName.trim() || this.editKeyPermissions.length === 0) return;
|
||||
|
||||
this.updating.set(true);
|
||||
this.apiService.updateKey(userId, key.keytoken_id, {
|
||||
name: this.editKeyName.trim(),
|
||||
permissions: this.editKeyPermissions.join(';'),
|
||||
all_channels: this.editKeyAllChannels,
|
||||
channels: this.editKeyAllChannels ? undefined : this.editKeyChannels
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.notification.success('Key updated');
|
||||
this.updating.set(false);
|
||||
this.closeEditModal();
|
||||
this.loadKeys();
|
||||
},
|
||||
error: () => {
|
||||
this.updating.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onEditPermissionChange(perm: TokenPermission, checked: boolean): void {
|
||||
if (checked) {
|
||||
if (perm === 'A') {
|
||||
this.editKeyPermissions = ['A'];
|
||||
} else if (!this.editKeyPermissions.includes(perm)) {
|
||||
this.editKeyPermissions = [...this.editKeyPermissions, perm];
|
||||
}
|
||||
} else {
|
||||
this.editKeyPermissions = this.editKeyPermissions.filter(p => p !== perm);
|
||||
}
|
||||
}
|
||||
|
||||
isEditPermissionChecked(perm: TokenPermission): boolean {
|
||||
return this.editKeyPermissions.includes(perm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,21 @@
|
||||
{{ getChannelDisplayName(channel) }}
|
||||
</nz-tag>
|
||||
}
|
||||
@for (sender of senderFilter; track sender) {
|
||||
<nz-tag nzMode="closeable" (nzOnClose)="removeSenderFilter(sender)">
|
||||
{{ sender }}
|
||||
</nz-tag>
|
||||
}
|
||||
@if (priorityFilter.length > 0) {
|
||||
<nz-tag nzMode="closeable" (nzOnClose)="clearPriorityFilter()">
|
||||
{{ getPriorityLabel(+priorityFilter[0]) }}
|
||||
</nz-tag>
|
||||
}
|
||||
@if (dateRange) {
|
||||
<nz-tag nzMode="closeable" (nzOnClose)="clearDateRange()">
|
||||
{{ getDateRangeDisplay() }}
|
||||
</nz-tag>
|
||||
}
|
||||
<a class="clear-all" (click)="clearAllFilters()">Clear all</a>
|
||||
</div>
|
||||
}
|
||||
@@ -64,14 +74,35 @@
|
||||
[nzFilterMultiple]="true"
|
||||
(nzFilterChange)="onChannelFilterChange($event)"
|
||||
>Channel</th>
|
||||
<th nzWidth="15%">Sender</th>
|
||||
<th
|
||||
nzWidth="15%"
|
||||
[nzFilters]="senderFilters()"
|
||||
[nzFilterMultiple]="true"
|
||||
(nzFilterChange)="onSenderFilterChange($event)"
|
||||
>Sender</th>
|
||||
<th
|
||||
nzWidth="10%"
|
||||
[nzFilters]="priorityFilters"
|
||||
[nzFilterMultiple]="false"
|
||||
(nzFilterChange)="onPriorityFilterChange($event)"
|
||||
>Priority</th>
|
||||
<th nzWidth="20%">Time</th>
|
||||
<th nzWidth="20%" nzCustomFilter>
|
||||
Time
|
||||
<nz-filter-trigger [(nzVisible)]="dateFilterVisible" [nzActive]="!!dateRange" [nzDropdownMenu]="dateMenu">
|
||||
<span nz-icon nzType="filter" nzTheme="fill"></span>
|
||||
</nz-filter-trigger>
|
||||
<nz-dropdown-menu #dateMenu="nzDropdownMenu">
|
||||
<div class="date-filter-dropdown" (click)="$event.stopPropagation()">
|
||||
<nz-range-picker
|
||||
[ngModel]="dateRange"
|
||||
(ngModelChange)="onDateRangeChange($event)"
|
||||
[nzAllowClear]="true"
|
||||
nzFormat="yyyy-MM-dd"
|
||||
[nzInline]="true"
|
||||
></nz-range-picker>
|
||||
</div>
|
||||
</nz-dropdown-menu>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.date-filter-dropdown {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.active-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -11,6 +11,8 @@ import { NzEmptyModule } from 'ng-zorro-antd/empty';
|
||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
|
||||
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
|
||||
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||
import { ApiService } from '../../../core/services/api.service';
|
||||
import { AuthService } from '../../../core/services/auth.service';
|
||||
import { Message, MessageListParams } from '../../../core/models';
|
||||
@@ -31,6 +33,8 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
|
||||
NzSpinModule,
|
||||
NzToolTipModule,
|
||||
NzPaginationModule,
|
||||
NzDatePickerModule,
|
||||
NzDropDownModule,
|
||||
RelativeTimePipe,
|
||||
],
|
||||
templateUrl: './message-list.component.html',
|
||||
@@ -54,6 +58,9 @@ export class MessageListComponent implements OnInit {
|
||||
appliedSearchText = '';
|
||||
priorityFilter: string[] = [];
|
||||
channelFilter: string[] = [];
|
||||
senderFilter: string[] = [];
|
||||
dateRange: [Date, Date] | null = null;
|
||||
dateFilterVisible = false;
|
||||
|
||||
// Filter options
|
||||
priorityFilters: NzTableFilterList = [
|
||||
@@ -62,9 +69,11 @@ export class MessageListComponent implements OnInit {
|
||||
{ text: 'High', value: '2' },
|
||||
];
|
||||
channelFilters = signal<NzTableFilterList>([]);
|
||||
senderFilters = signal<NzTableFilterList>([]);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadChannels();
|
||||
this.loadSenders();
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
@@ -77,7 +86,20 @@ export class MessageListComponent implements OnInit {
|
||||
this.channelFilters.set(
|
||||
response.channels.map(ch => ({
|
||||
text: ch.display_name,
|
||||
value: ch.internal_name,
|
||||
value: ch.channel_id,
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadSenders(): void {
|
||||
this.apiService.getSenderNames().subscribe({
|
||||
next: (response) => {
|
||||
this.senderFilters.set(
|
||||
response.sender_names.map(s => ({
|
||||
text: s.name,
|
||||
value: s.name,
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -95,11 +117,18 @@ export class MessageListComponent implements OnInit {
|
||||
if (this.appliedSearchText) {
|
||||
params.search = this.appliedSearchText;
|
||||
}
|
||||
if (this.priorityFilter.length === 1) {
|
||||
params.priority = parseInt(this.priorityFilter[0], 10);
|
||||
if (this.priorityFilter.length > 0) {
|
||||
params.priority = this.priorityFilter.map(p => parseInt(p, 10));
|
||||
}
|
||||
if (this.channelFilter.length > 0) {
|
||||
params.channel = this.channelFilter.join(',');
|
||||
params.channel_id = this.channelFilter;
|
||||
}
|
||||
if (this.senderFilter.length > 0) {
|
||||
params.sender = this.senderFilter;
|
||||
}
|
||||
if (this.dateRange) {
|
||||
params.after = this.dateRange[0].toISOString();
|
||||
params.before = this.dateRange[1].toISOString();
|
||||
}
|
||||
|
||||
// Use page-index based pagination: $1 = page 1, $2 = page 2, etc.
|
||||
@@ -138,6 +167,12 @@ export class MessageListComponent implements OnInit {
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
onSenderFilterChange(filters: string[] | null): void {
|
||||
this.senderFilter = filters ?? [];
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
clearSearch(): void {
|
||||
this.searchText = '';
|
||||
this.appliedSearchText = '';
|
||||
@@ -157,29 +192,64 @@ export class MessageListComponent implements OnInit {
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
clearSenderFilter(): void {
|
||||
this.senderFilter = [];
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
removeSenderFilter(sender: string): void {
|
||||
this.senderFilter = this.senderFilter.filter(s => s !== sender);
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
clearPriorityFilter(): void {
|
||||
this.priorityFilter = [];
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
onDateRangeChange(dates: [Date, Date] | null): void {
|
||||
this.dateRange = dates;
|
||||
if (dates) {
|
||||
this.dateFilterVisible = false;
|
||||
}
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
clearDateRange(): void {
|
||||
this.dateRange = null;
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
clearAllFilters(): void {
|
||||
this.searchText = '';
|
||||
this.appliedSearchText = '';
|
||||
this.channelFilter = [];
|
||||
this.senderFilter = [];
|
||||
this.priorityFilter = [];
|
||||
this.dateRange = null;
|
||||
this.currentPage.set(1);
|
||||
this.loadMessages();
|
||||
}
|
||||
|
||||
hasActiveFilters(): boolean {
|
||||
return !!this.appliedSearchText || this.channelFilter.length > 0 || this.priorityFilter.length > 0;
|
||||
return !!this.appliedSearchText || this.channelFilter.length > 0 || this.senderFilter.length > 0 || this.priorityFilter.length > 0 || !!this.dateRange;
|
||||
}
|
||||
|
||||
getChannelDisplayName(internalName: string): string {
|
||||
getChannelDisplayName(channelId: string): string {
|
||||
const filters = this.channelFilters();
|
||||
const channel = filters.find(f => f.value === internalName);
|
||||
return channel?.text?.toString() ?? internalName;
|
||||
const channel = filters.find(f => f.value === channelId);
|
||||
return channel?.text?.toString() ?? channelId;
|
||||
}
|
||||
|
||||
getDateRangeDisplay(): string {
|
||||
if (!this.dateRange) return '';
|
||||
const format = (d: Date) => d.toLocaleDateString();
|
||||
return `${format(this.dateRange[0])} - ${format(this.dateRange[1])}`;
|
||||
}
|
||||
|
||||
goToPage(page: number): void {
|
||||
|
||||
Reference in New Issue
Block a user