mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 04:07:39 +00:00
v21 updates
This commit is contained in:
@@ -8,3 +8,5 @@
|
|||||||
/.vs/WinStudentGoalTracker/DesignTimeBuild
|
/.vs/WinStudentGoalTracker/DesignTimeBuild
|
||||||
/.vs/WinStudentGoalTracker/v17
|
/.vs/WinStudentGoalTracker/v17
|
||||||
/.vs
|
/.vs
|
||||||
|
*.md
|
||||||
|
/db/Scripts
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "4kB",
|
"maximumWarning": "6kB",
|
||||||
"maximumError": "8kB"
|
"maximumError": "10kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|||||||
+1374
-1105
File diff suppressed because it is too large
Load Diff
@@ -20,27 +20,27 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^20.1.0",
|
"@angular/common": "^21.2.10",
|
||||||
"@angular/compiler": "^20.1.0",
|
"@angular/compiler": "^21.2.10",
|
||||||
"@angular/core": "^20.1.0",
|
"@angular/core": "^21.2.10",
|
||||||
"@angular/forms": "^20.1.0",
|
"@angular/forms": "^21.2.10",
|
||||||
"@angular/platform-browser": "^20.1.0",
|
"@angular/platform-browser": "^21.2.10",
|
||||||
"@angular/router": "^20.1.0",
|
"@angular/router": "^21.2.10",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.2",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.16.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.1.5",
|
"@angular/build": "^21.2.8",
|
||||||
"@angular/cli": "^20.1.5",
|
"@angular/cli": "^21.2.8",
|
||||||
"@angular/compiler-cli": "^20.1.0",
|
"@angular/compiler-cli": "^21.2.10",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~6.0.0",
|
||||||
"jasmine-core": "~5.8.0",
|
"jasmine-core": "~6.2.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.2.0",
|
||||||
"typescript": "~5.8.2"
|
"typescript": "~6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ export class ModalShell implements OnInit, OnDestroy {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.previousFocus = document.activeElement as HTMLElement;
|
this.previousFocus = document.activeElement as HTMLElement;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const focusable = this.el.nativeElement.querySelector<HTMLElement>(
|
const focusable = this.el.nativeElement.querySelector(
|
||||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
);
|
) as HTMLElement | null;
|
||||||
focusable?.focus();
|
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