feat: implement tenant branding system and global toast notification service

This commit is contained in:
2026-07-01 11:54:11 -03:00
parent 2764201ca0
commit 0363280ede
17 changed files with 294 additions and 5 deletions

View File

@@ -17,6 +17,7 @@ export const DEFAULT_TENANT_BRANDING = {
primaryColor: '#102bda',
secondaryColor: '#9e9e9e',
dangerColor: '#fd4c4c',
successColor: '#198754',
headerFooterBgColor: '#313131'
} as const;

View File

@@ -8,6 +8,7 @@ export interface Tenant {
primary_color: string;
secondary_color: string;
danger_color: string;
success_color: string;
header_footer_bg_color: string;
header_logo: string;
footer_logo: string;

View File

@@ -16,6 +16,7 @@ const tenant: Tenant = {
primary_color: '#6376F3',
secondary_color: '#A0A0A0',
danger_color: '#FF8888',
success_color: '#198754',
header_footer_bg_color: '#313131',
header_logo: 'https://example.com/header.png',
footer_logo: 'https://example.com/footer.png'

View File

@@ -0,0 +1,101 @@
import { TestBed } from '@angular/core/testing';
import { vi } from 'vitest';
import { ToastService } from './toast.service';
describe('ToastService', () => {
let service: ToastService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ToastService]
});
service = TestBed.inject(ToastService);
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should initialize with an empty list of toasts', () => {
expect(service.toasts()).toEqual([]);
});
it('should show a toast and assign an id', () => {
const id = service.show('Hello world', 'info');
expect(id).toBeTruthy();
expect(service.toasts().length).toBe(1);
expect(service.toasts()[0]).toEqual({
id,
message: 'Hello world',
type: 'info',
duration: 3000
});
});
it('should add specific variant toasts via helper methods', () => {
const infoId = service.info('Info message');
const dangerId = service.danger('Danger message', 5000);
const successId = service.success('Success message', 0);
const toasts = service.toasts();
expect(toasts.length).toBe(3);
expect(toasts.find((t) => t.id === infoId)).toEqual({
id: infoId,
message: 'Info message',
type: 'info',
duration: 3000
});
expect(toasts.find((t) => t.id === dangerId)).toEqual({
id: dangerId,
message: 'Danger message',
type: 'danger',
duration: 5000
});
expect(toasts.find((t) => t.id === successId)).toEqual({
id: successId,
message: 'Success message',
type: 'success',
duration: 0
});
});
it('should dismiss a toast manually by id', () => {
const id1 = service.show('Toast 1');
const id2 = service.show('Toast 2');
expect(service.toasts().length).toBe(2);
service.dismiss(id1);
expect(service.toasts().length).toBe(1);
expect(service.toasts()[0].id).toBe(id2);
});
it('should automatically dismiss a toast after the duration', () => {
service.show('Auto dismiss toast', 'info', 2000);
expect(service.toasts().length).toBe(1);
vi.advanceTimersByTime(2000);
expect(service.toasts().length).toBe(0);
});
it('should not dismiss a toast automatically if duration is 0', () => {
service.show('Persistent toast', 'info', 0);
expect(service.toasts().length).toBe(1);
vi.advanceTimersByTime(10000);
expect(service.toasts().length).toBe(1);
});
});

View File

@@ -0,0 +1,47 @@
import { Injectable, signal } from '@angular/core';
export interface Toast {
id: string;
message: string;
type: 'success' | 'danger' | 'info';
duration?: number;
}
@Injectable({
providedIn: 'root'
})
export class ToastService {
private readonly toastsSignal = signal<Toast[]>([]);
readonly toasts = this.toastsSignal.asReadonly();
show(message: string, type: 'success' | 'danger' | 'info' = 'info', duration = 3000): string {
const id = Math.random().toString(36).substring(2, 9);
const newToast: Toast = { id, message, type, duration };
this.toastsSignal.update((toasts) => [...toasts, newToast]);
if (duration > 0) {
setTimeout(() => {
this.dismiss(id);
}, duration);
}
return id;
}
info(message: string, duration = 3000): string {
return this.show(message, 'info', duration);
}
danger(message: string, duration = 3000): string {
return this.show(message, 'danger', duration);
}
success(message: string, duration = 3000): string {
return this.show(message, 'success', duration);
}
dismiss(id: string): void {
this.toastsSignal.update((toasts) => toasts.filter((t) => t.id !== id));
}
}