first commit

This commit is contained in:
Mateusz Gruszczyński
2026-04-05 13:40:27 +02:00
commit 9a6e77a5fc
89 changed files with 18276 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
import { CommonModule, DatePipe } from '@angular/common';
import { Component, OnInit, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { AdminService } from '../../core/services/admin.service';
import { AppSettingsService } from '../../core/services/app-settings.service';
import { ToastService } from '../../core/services/toast.service';
import type { AppSettings, User } from '../../shared/models';
@Component({
selector: 'app-admin',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, DatePipe],
template: `
<div class="page-header d-print-none mb-3 ec-page-header">
<div class="row align-items-center g-3">
<div class="col">
<h2 class="page-title mb-1">Administracja</h2>
<div class="text-secondary">Ustawienia aplikacji, SMTP oraz zarządzanie użytkownikami.</div>
</div>
</div>
</div>
<div class="row row-cards align-items-start">
<div class="col-xl-5">
<div class="card pv-card overflow-hidden">
<div class="card-header"><h3 class="card-title">Ustawienia aplikacji</h3></div>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="save()" class="d-grid gap-3">
<div>
<label class="form-label">Nazwa aplikacji</label>
<input class="form-control" formControlName="appName" />
</div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Domyślna waluta</label><input class="form-control" formControlName="defaultCurrency" /></div>
<div class="col-md-6"><label class="form-label">Typy potwierdzeń</label><input class="form-control" formControlName="allowedProofTypes" /></div>
</div>
<label class="form-check">
<input class="form-check-input" type="checkbox" formControlName="registrationEnabled" />
<span class="form-check-label">Włącz rejestrację</span>
</label>
<hr class="my-2" />
<div class="fw-semibold">SMTP</div>
<label class="form-check">
<input class="form-check-input" type="checkbox" formControlName="smtpEnabled" />
<span class="form-check-label">Włącz SMTP</span>
</label>
<div class="row g-3">
<div class="col-md-7"><label class="form-label">Host</label><input class="form-control" formControlName="smtpHost" /></div>
<div class="col-md-5"><label class="form-label">Port</label><input class="form-control" type="number" formControlName="smtpPort" /></div>
<div class="col-md-6"><label class="form-label">Użytkownik</label><input class="form-control" formControlName="smtpUser" /></div>
<div class="col-md-6"><label class="form-label">Hasło</label><input class="form-control" type="password" formControlName="smtpPassword" /></div>
<div class="col-md-6"><label class="form-label">Nazwa nadawcy</label><input class="form-control" formControlName="smtpFromName" /></div>
<div class="col-md-6"><label class="form-label">E-mail nadawcy</label><input class="form-control" formControlName="smtpFromEmail" /></div>
</div>
<label class="form-check">
<input class="form-check-input" type="checkbox" formControlName="smtpSecure" />
<span class="form-check-label">Bezpieczne połączenie</span>
</label>
<div class="btn-list flex-wrap">
<button class="btn btn-success d-inline-flex align-items-center gap-2" [disabled]="form.invalid || saving()">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10"/></svg>
<span>Zapisz</span>
</button>
<button class="btn btn-outline-info" type="button" (click)="sendTest()">Test SMTP</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-7">
<div class="card pv-card overflow-hidden">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title">Użytkownicy</h3>
<span class="badge bg-dark-lt">{{ users().length }}</span>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table mb-0">
<thead><tr><th>Użytkownik</th><th>Rola</th><th>Status</th><th>Data</th><th class="w-1"></th></tr></thead>
<tbody>
@for (user of users(); track user.id) {
<tr>
<td>
<div class="fw-semibold">{{ user.fullName }}</div>
<div class="small text-secondary">{{ user.email }}</div>
</td>
<td>{{ user.role }}</td>
<td>
<span class="badge" [class.bg-success]="user.isActive" [class.bg-secondary]="!user.isActive">
{{ user.isActive ? 'Aktywny' : 'Zablokowany' }}
</span>
</td>
<td>{{ user.createdAt | date:'short' }}</td>
<td>
<div class="btn-list flex-nowrap">
<button class="btn btn-outline-warning btn-sm" type="button" (click)="toggleRole(user)">
{{ user.role === 'ADMIN' ? 'Ustaw USER' : 'Ustaw ADMIN' }}
</button>
<button class="btn btn-sm" [class.btn-danger]="user.isActive" [class.btn-success]="!user.isActive" type="button" (click)="toggleActive(user)">
{{ user.isActive ? 'Zablokuj' : 'Odblokuj' }}
</button>
</div>
</td>
</tr>
} @empty {
<tr><td colspan="5" class="text-secondary">Brak użytkowników.</td></tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
`
})
export class AdminComponent implements OnInit {
private readonly fb = inject(FormBuilder);
private readonly admin = inject(AdminService);
private readonly appSettings = inject(AppSettingsService);
private readonly toast = inject(ToastService);
readonly users = signal<User[]>([]);
readonly settings = signal<AppSettings | null>(null);
readonly saving = signal(false);
readonly form = this.fb.nonNullable.group({
appName: ['', [Validators.required, Validators.minLength(2)]],
defaultCurrency: ['PLN', Validators.required],
allowedProofTypes: ['RECEIPT,INVOICE,NOTE,BANK_STATEMENT,OTHER', Validators.required],
registrationEnabled: [true],
smtpEnabled: [false],
smtpHost: [''],
smtpPort: [587],
smtpSecure: [false],
smtpUser: [''],
smtpPassword: [''],
smtpFromName: [''],
smtpFromEmail: ['']
});
ngOnInit() {
this.load();
}
load() {
this.admin.getSettings().subscribe({
next: (response) => {
this.settings.set(response.item);
this.appSettings.applySettings(response.item);
this.form.reset({
appName: response.item.appName,
defaultCurrency: response.item.defaultCurrency,
allowedProofTypes: response.item.allowedProofTypes.join(','),
registrationEnabled: response.item.registrationEnabled,
smtpEnabled: response.item.smtpEnabled,
smtpHost: response.item.smtpHost ?? '',
smtpPort: response.item.smtpPort,
smtpSecure: response.item.smtpSecure,
smtpUser: response.item.smtpUser ?? '',
smtpPassword: response.item.smtpPassword ?? '',
smtpFromName: response.item.smtpFromName ?? '',
smtpFromEmail: response.item.smtpFromEmail ?? ''
});
}
});
this.admin.listUsers().subscribe({ next: (response) => this.users.set(response.items) });
}
save() {
if (this.form.invalid) return;
this.saving.set(true);
const raw = this.form.getRawValue();
this.admin
.updateSettings({
appName: raw.appName,
defaultCurrency: raw.defaultCurrency,
registrationEnabled: raw.registrationEnabled,
allowedProofTypes: raw.allowedProofTypes.split(',').map((item) => item.trim()).filter(Boolean),
uiPreferences: { theme: 'dark', density: 'comfortable', defaultStatsPeriod: 'month' },
smtpEnabled: raw.smtpEnabled,
smtpHost: raw.smtpHost || null,
smtpPort: Number(raw.smtpPort),
smtpSecure: raw.smtpSecure,
smtpUser: raw.smtpUser || null,
smtpPassword: raw.smtpPassword || null,
smtpFromName: raw.smtpFromName || null,
smtpFromEmail: raw.smtpFromEmail || null
})
.subscribe({
next: (response) => {
this.saving.set(false);
this.settings.set(response.item);
this.appSettings.applySettings(response.item);
this.toast.success('Ustawienia zapisane.');
},
error: (error) => {
this.saving.set(false);
this.toast.error(error.error?.message ?? 'Nie udało się zapisać ustawień.');
}
});
}
sendTest() {
const to = this.form.getRawValue().smtpFromEmail;
if (!to) {
this.toast.error('Uzupełnij e-mail nadawcy.');
return;
}
this.admin.testSmtp(to).subscribe({
next: () => this.toast.success('Wiadomość testowa została wysłana.'),
error: (error) => this.toast.error(error.error?.message ?? 'Nie udało się wysłać testu SMTP.')
});
}
toggleRole(user: User) {
this.admin.updateUser(user.id, { role: user.role === 'ADMIN' ? 'USER' : 'ADMIN' }).subscribe({
next: (response) => {
this.users.update((items) => items.map((item) => (item.id === user.id ? response.item : item)));
this.toast.success('Rola została zaktualizowana.');
},
error: (error) => this.toast.error(error.error?.message ?? 'Nie udało się zmienić roli.')
});
}
toggleActive(user: User) {
this.admin.updateUser(user.id, { isActive: !user.isActive }).subscribe({
next: (response) => {
this.users.update((items) => items.map((item) => (item.id === user.id ? response.item : item)));
this.toast.success('Status konta został zaktualizowany.');
},
error: (error) => this.toast.error(error.error?.message ?? 'Nie udało się zmienić statusu.')
});
}
}