mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 02:57:36 +00:00
v21 updates
This commit is contained in:
@@ -15,9 +15,9 @@ export class ModalShell implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.previousFocus = document.activeElement as HTMLElement;
|
||||
requestAnimationFrame(() => {
|
||||
const focusable = this.el.nativeElement.querySelector<HTMLElement>(
|
||||
const focusable = this.el.nativeElement.querySelector(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
) as HTMLElement | null;
|
||||
focusable?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100dvh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 1rem;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #4338CA;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
padding: 0.625rem 1rem;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
button[type='submit']:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
display: inline-block;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4f46e5;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 0;
|
||||
margin-top: 0.75rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ResponseResult } from '../classes/auth.models';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface ProgramDto {
|
||||
programId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DistrictUserDto {
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
programId: string;
|
||||
programName: string;
|
||||
roleId: string;
|
||||
roleName: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface RoleDto {
|
||||
roleId: string;
|
||||
name: string;
|
||||
internalName: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AdminService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = environment.apiBaseUrl;
|
||||
|
||||
// ************************ Programs *************************
|
||||
|
||||
async getPrograms(): Promise<ProgramDto[]> {
|
||||
const result = await firstValueFrom(
|
||||
this.http.get<ResponseResult<ProgramDto[]>>(`${this.base}/api/Admin/programs`)
|
||||
);
|
||||
return result.success && result.data ? result.data : [];
|
||||
}
|
||||
|
||||
async createProgram(name: string, description?: string): Promise<ResponseResult<object>> {
|
||||
return await firstValueFrom(
|
||||
this.http.post<ResponseResult<object>>(`${this.base}/api/Admin/programs`, { name, description })
|
||||
);
|
||||
}
|
||||
|
||||
async updateProgram(programId: string, name: string, description?: string): Promise<ResponseResult<object>> {
|
||||
return await firstValueFrom(
|
||||
this.http.put<ResponseResult<object>>(`${this.base}/api/Admin/programs/${programId}`, { name, description })
|
||||
);
|
||||
}
|
||||
|
||||
// ************************ Users *************************
|
||||
|
||||
async getUsers(): Promise<DistrictUserDto[]> {
|
||||
const result = await firstValueFrom(
|
||||
this.http.get<ResponseResult<DistrictUserDto[]>>(`${this.base}/api/Admin/users`)
|
||||
);
|
||||
return result.success && result.data ? result.data : [];
|
||||
}
|
||||
|
||||
async createUser(email: string, name: string, password: string, programId: string, roleId: string): Promise<ResponseResult<object>> {
|
||||
return await firstValueFrom(
|
||||
this.http.post<ResponseResult<object>>(`${this.base}/api/Admin/users`, { email, name, password, programId, roleId })
|
||||
);
|
||||
}
|
||||
|
||||
// ************************ Roles *************************
|
||||
|
||||
async getRoles(): Promise<RoleDto[]> {
|
||||
const result = await firstValueFrom(
|
||||
this.http.get<ResponseResult<RoleDto[]>>(`${this.base}/api/Admin/roles`)
|
||||
);
|
||||
return result.success && result.data ? result.data : [];
|
||||
}
|
||||
|
||||
// ************************ Backup *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Downloads a full database backup as a .sql file. The API streams
|
||||
// the dump as a blob; this method triggers a browser file-save
|
||||
// dialog using a temporary anchor element.
|
||||
// *****************************************************************
|
||||
async backupDatabase(): Promise<void> {
|
||||
const blob = await firstValueFrom(
|
||||
this.http.get(`${this.base}/api/Admin/backup`, { responseType: 'blob' })
|
||||
);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
a.download = `winstudentgoaltracker_backup_${timestamp}.sql`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user