Simple Managment webapp [LLM]

This commit is contained in:
2025-12-03 19:38:15 +01:00
parent 3ed323e056
commit 6090319b5f
10 changed files with 468 additions and 31 deletions

View File

@@ -18,10 +18,10 @@ export interface Message {
export interface MessageListParams { export interface MessageListParams {
after?: string; after?: string;
before?: string; before?: string;
channel?: string; channel_id?: string[];
priority?: number; priority?: number[];
search?: string; search?: string;
sender?: string; sender?: string[];
subscription_status?: 'all' | 'confirmed' | 'unconfirmed'; subscription_status?: 'all' | 'confirmed' | 'unconfirmed';
trimmed?: boolean; trimmed?: boolean;
page_size?: number; page_size?: number;

View File

@@ -135,10 +135,22 @@ export class ApiService {
if (params) { if (params) {
if (params.after) httpParams = httpParams.set('after', params.after); if (params.after) httpParams = httpParams.set('after', params.after);
if (params.before) httpParams = httpParams.set('before', params.before); if (params.before) httpParams = httpParams.set('before', params.before);
if (params.channel) httpParams = httpParams.set('channel', params.channel); if (params.channel_id) {
if (params.priority !== undefined) httpParams = httpParams.set('priority', params.priority); 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.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.subscription_status) httpParams = httpParams.set('subscription_status', params.subscription_status);
if (params.trimmed !== undefined) httpParams = httpParams.set('trimmed', params.trimmed); if (params.trimmed !== undefined) httpParams = httpParams.set('trimmed', params.trimmed);
if (params.page_size) httpParams = httpParams.set('page_size', params.page_size); if (params.page_size) httpParams = httpParams.set('page_size', params.page_size);

View 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;
}
}

View File

@@ -69,7 +69,7 @@
</form> </form>
<div class="login-footer"> <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> </div>
</nz-card> </nz-card>
</div> </div>

View File

@@ -59,6 +59,12 @@
<nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels"> <nz-tag nzColor="default" nz-tooltip nzTooltipTitle="Access to all channels">
All Channels All Channels
</nz-tag> </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> </div>
</td> </td>
@@ -73,22 +79,29 @@
} }
</td> </td>
<td> <td>
@if (!isCurrentKey(key)) { <div class="action-buttons">
<button <button
nz-button nz-button
nzSize="small" nzSize="small"
nzDanger nz-tooltip
nz-popconfirm nzTooltipTitle="Edit key"
nzPopconfirmTitle="Are you sure you want to delete this key?" (click)="openEditModal(key)"
(nzOnConfirm)="deleteKey(key)"
> >
<span nz-icon nzType="delete"></span> <span nz-icon nzType="edit"></span>
</button> </button>
} @else { @if (!isCurrentKey(key)) {
<span class="text-muted" nz-tooltip nzTooltipTitle="Cannot delete the key you're currently using"> <button
- nz-button
</span> 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> </td>
</tr> </tr>
} @empty { } @empty {
@@ -180,11 +193,33 @@
</nz-form-control> </nz-form-control>
</nz-form-item> </nz-form-item>
<nz-form-item class="mb-0"> <nz-form-item>
<label nz-checkbox [(ngModel)]="newKeyAllChannels"> <label nz-checkbox [(ngModel)]="newKeyAllChannels">
Access to all channels Access to all channels
</label> </label>
</nz-form-item> </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> </ng-container>
</nz-modal> </nz-modal>
@@ -205,3 +240,87 @@
</button> </button>
} }
</ng-template> </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>

View File

@@ -42,6 +42,11 @@
color: #999; color: #999;
} }
.action-buttons {
display: flex;
gap: 8px;
}
.copy-icon { .copy-icon {
cursor: pointer; cursor: pointer;
color: #999; color: #999;

View File

@@ -14,10 +14,12 @@ import { NzInputModule } from 'ng-zorro-antd/input';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'; import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { NzAlertModule } from 'ng-zorro-antd/alert'; import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { ApiService } from '../../../core/services/api.service'; import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service'; import { AuthService } from '../../../core/services/auth.service';
import { NotificationService } from '../../../core/services/notification.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 { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive'; import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
@@ -46,6 +48,7 @@ interface PermissionOption {
NzCheckboxModule, NzCheckboxModule,
NzToolTipModule, NzToolTipModule,
NzAlertModule, NzAlertModule,
NzSelectModule,
RelativeTimePipe, RelativeTimePipe,
CopyToClipboardDirective, CopyToClipboardDirective,
], ],
@@ -56,19 +59,32 @@ export class KeyListComponent implements OnInit {
private apiService = inject(ApiService); private apiService = inject(ApiService);
private authService = inject(AuthService); private authService = inject(AuthService);
private notification = inject(NotificationService); private notification = inject(NotificationService);
private channelCacheService = inject(ChannelCacheService);
keys = signal<KeyToken[]>([]); keys = signal<KeyToken[]>([]);
currentKeyId = signal<string | null>(null); currentKeyId = signal<string | null>(null);
loading = signal(false); loading = signal(false);
channelNames = signal<Map<string, ResolvedChannel>>(new Map());
availableChannels = signal<ChannelWithSubscription[]>([]);
// Create modal // Create modal
showCreateModal = signal(false); showCreateModal = signal(false);
newKeyName = ''; newKeyName = '';
newKeyPermissions: TokenPermission[] = ['CR']; newKeyPermissions: TokenPermission[] = ['CR'];
newKeyAllChannels = true; newKeyAllChannels = true;
newKeyChannels: string[] = [];
creating = signal(false); creating = signal(false);
createdKey = signal<KeyToken | null>(null); 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[] = [ permissionOptions: PermissionOption[] = [
{ value: 'A', label: 'Admin', description: 'Full access to all operations' }, { value: 'A', label: 'Admin', description: 'Full access to all operations' },
{ value: 'CR', label: 'Channel Read', description: 'Read messages from channels' }, { value: 'CR', label: 'Channel Read', description: 'Read messages from channels' },
@@ -79,6 +95,17 @@ export class KeyListComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.loadKeys(); this.loadKeys();
this.loadCurrentKey(); 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 { loadKeys(): void {
@@ -90,6 +117,7 @@ export class KeyListComponent implements OnInit {
next: (response) => { next: (response) => {
this.keys.set(response.keys); this.keys.set(response.keys);
this.loading.set(false); this.loading.set(false);
this.resolveChannelNames(response.keys);
}, },
error: () => { error: () => {
this.loading.set(false); 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 { loadCurrentKey(): void {
const userId = this.authService.getUserId(); const userId = this.authService.getUserId();
if (!userId) return; if (!userId) return;
@@ -134,6 +184,7 @@ export class KeyListComponent implements OnInit {
this.newKeyName = ''; this.newKeyName = '';
this.newKeyPermissions = ['CR']; this.newKeyPermissions = ['CR'];
this.newKeyAllChannels = true; this.newKeyAllChannels = true;
this.newKeyChannels = [];
this.createdKey.set(null); this.createdKey.set(null);
this.showCreateModal.set(true); this.showCreateModal.set(true);
} }
@@ -150,7 +201,8 @@ export class KeyListComponent implements OnInit {
this.apiService.createKey(userId, { this.apiService.createKey(userId, {
name: this.newKeyName.trim(), name: this.newKeyName.trim(),
permissions: this.newKeyPermissions.join(';'), permissions: this.newKeyPermissions.join(';'),
all_channels: this.newKeyAllChannels all_channels: this.newKeyAllChannels,
channels: this.newKeyAllChannels ? undefined : this.newKeyChannels
}).subscribe({ }).subscribe({
next: (key) => { next: (key) => {
this.createdKey.set(key); this.createdKey.set(key);
@@ -198,4 +250,59 @@ export class KeyListComponent implements OnInit {
isPermissionChecked(perm: TokenPermission): boolean { isPermissionChecked(perm: TokenPermission): boolean {
return this.newKeyPermissions.includes(perm); 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);
}
} }

View File

@@ -36,11 +36,21 @@
{{ getChannelDisplayName(channel) }} {{ getChannelDisplayName(channel) }}
</nz-tag> </nz-tag>
} }
@for (sender of senderFilter; track sender) {
<nz-tag nzMode="closeable" (nzOnClose)="removeSenderFilter(sender)">
{{ sender }}
</nz-tag>
}
@if (priorityFilter.length > 0) { @if (priorityFilter.length > 0) {
<nz-tag nzMode="closeable" (nzOnClose)="clearPriorityFilter()"> <nz-tag nzMode="closeable" (nzOnClose)="clearPriorityFilter()">
{{ getPriorityLabel(+priorityFilter[0]) }} {{ getPriorityLabel(+priorityFilter[0]) }}
</nz-tag> </nz-tag>
} }
@if (dateRange) {
<nz-tag nzMode="closeable" (nzOnClose)="clearDateRange()">
{{ getDateRangeDisplay() }}
</nz-tag>
}
<a class="clear-all" (click)="clearAllFilters()">Clear all</a> <a class="clear-all" (click)="clearAllFilters()">Clear all</a>
</div> </div>
} }
@@ -64,14 +74,35 @@
[nzFilterMultiple]="true" [nzFilterMultiple]="true"
(nzFilterChange)="onChannelFilterChange($event)" (nzFilterChange)="onChannelFilterChange($event)"
>Channel</th> >Channel</th>
<th nzWidth="15%">Sender</th> <th
nzWidth="15%"
[nzFilters]="senderFilters()"
[nzFilterMultiple]="true"
(nzFilterChange)="onSenderFilterChange($event)"
>Sender</th>
<th <th
nzWidth="10%" nzWidth="10%"
[nzFilters]="priorityFilters" [nzFilters]="priorityFilters"
[nzFilterMultiple]="false" [nzFilterMultiple]="false"
(nzFilterChange)="onPriorityFilterChange($event)" (nzFilterChange)="onPriorityFilterChange($event)"
>Priority</th> >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> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -13,6 +13,11 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
.date-filter-dropdown {
padding: 12px;
background: #fff;
}
.active-filters { .active-filters {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -11,6 +11,8 @@ import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzSpinModule } from 'ng-zorro-antd/spin'; import { NzSpinModule } from 'ng-zorro-antd/spin';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { NzPaginationModule } from 'ng-zorro-antd/pagination'; 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 { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service'; import { AuthService } from '../../../core/services/auth.service';
import { Message, MessageListParams } from '../../../core/models'; import { Message, MessageListParams } from '../../../core/models';
@@ -31,6 +33,8 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
NzSpinModule, NzSpinModule,
NzToolTipModule, NzToolTipModule,
NzPaginationModule, NzPaginationModule,
NzDatePickerModule,
NzDropDownModule,
RelativeTimePipe, RelativeTimePipe,
], ],
templateUrl: './message-list.component.html', templateUrl: './message-list.component.html',
@@ -54,6 +58,9 @@ export class MessageListComponent implements OnInit {
appliedSearchText = ''; appliedSearchText = '';
priorityFilter: string[] = []; priorityFilter: string[] = [];
channelFilter: string[] = []; channelFilter: string[] = [];
senderFilter: string[] = [];
dateRange: [Date, Date] | null = null;
dateFilterVisible = false;
// Filter options // Filter options
priorityFilters: NzTableFilterList = [ priorityFilters: NzTableFilterList = [
@@ -62,9 +69,11 @@ export class MessageListComponent implements OnInit {
{ text: 'High', value: '2' }, { text: 'High', value: '2' },
]; ];
channelFilters = signal<NzTableFilterList>([]); channelFilters = signal<NzTableFilterList>([]);
senderFilters = signal<NzTableFilterList>([]);
ngOnInit(): void { ngOnInit(): void {
this.loadChannels(); this.loadChannels();
this.loadSenders();
this.loadMessages(); this.loadMessages();
} }
@@ -77,7 +86,20 @@ export class MessageListComponent implements OnInit {
this.channelFilters.set( this.channelFilters.set(
response.channels.map(ch => ({ response.channels.map(ch => ({
text: ch.display_name, 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) { if (this.appliedSearchText) {
params.search = this.appliedSearchText; params.search = this.appliedSearchText;
} }
if (this.priorityFilter.length === 1) { if (this.priorityFilter.length > 0) {
params.priority = parseInt(this.priorityFilter[0], 10); params.priority = this.priorityFilter.map(p => parseInt(p, 10));
} }
if (this.channelFilter.length > 0) { 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. // Use page-index based pagination: $1 = page 1, $2 = page 2, etc.
@@ -138,6 +167,12 @@ export class MessageListComponent implements OnInit {
this.loadMessages(); this.loadMessages();
} }
onSenderFilterChange(filters: string[] | null): void {
this.senderFilter = filters ?? [];
this.currentPage.set(1);
this.loadMessages();
}
clearSearch(): void { clearSearch(): void {
this.searchText = ''; this.searchText = '';
this.appliedSearchText = ''; this.appliedSearchText = '';
@@ -157,29 +192,64 @@ export class MessageListComponent implements OnInit {
this.loadMessages(); 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 { clearPriorityFilter(): void {
this.priorityFilter = []; this.priorityFilter = [];
this.currentPage.set(1); this.currentPage.set(1);
this.loadMessages(); 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 { clearAllFilters(): void {
this.searchText = ''; this.searchText = '';
this.appliedSearchText = ''; this.appliedSearchText = '';
this.channelFilter = []; this.channelFilter = [];
this.senderFilter = [];
this.priorityFilter = []; this.priorityFilter = [];
this.dateRange = null;
this.currentPage.set(1); this.currentPage.set(1);
this.loadMessages(); this.loadMessages();
} }
hasActiveFilters(): boolean { 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 filters = this.channelFilters();
const channel = filters.find(f => f.value === internalName); const channel = filters.find(f => f.value === channelId);
return channel?.text?.toString() ?? internalName; 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 { goToPage(page: number): void {