feat: Made additions to grades. Closes #4

This commit is contained in:
2025-05-03 18:47:03 +02:00
parent c767d55e0e
commit 2fc4134acc
21 changed files with 792 additions and 362 deletions

811
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -228,6 +228,17 @@ export class AdminCommService {
getSummary: (start: moment.Moment, end: moment.Moment) => {
return this.http.get<{room: number, avg: number}[]>(environment.apiEndpoint+`/admin/clean/summary/${start.toISOString()}/${end.toISOString()}`, {withCredentials: true})
}
},
attendence: {
getUsers: (room: string) => {
return this.http.get<{users: {fname: string, surname: string, _id: string}[], attendence?: {id: string, hour?: string}[]}>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, {withCredentials: true})
},
postAttendence: (room: string, attendence: {id: string, hour?: string}[]) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, attendence, {withCredentials: true})
},
getSummary: () => {
return this.http.get<{room: string, hours: string[]}[]>(environment.apiEndpoint+`/admin/clean/attendenceSummary`, {withCredentials: true})
}
}
}
//#endregion

View File

@@ -0,0 +1,21 @@
<div id="guide">
<p><b>Uwaga:</b> Obecność resetuje się o codziennie o 00:00</p>
<div id="legend">
<b>Legenda: </b>
<span class="circle">Wychowanek obecny</span>
<span class="circle">Wyjście w ciągu 30 min.</span>
<span class="circle">Wychowanek nieobecny</span>
</div>
</div>
<table mat-table [dataSource]="data" matSort>
<div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Pokój</th>
<td mat-cell *matCellDef="let item">{{item.room}}</td>
</div>
<div matColumnDef="hours">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Godziny</th>
<td mat-cell *matCellDef="let item"><span *ngFor="let i of item.hours.sort().reverse(); let isLast=last"><app-hour-display [value]="i"></app-hour-display>{{ isLast ? '' : ', '}}</span></td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let rowData; columns: collumns"></tr>
</table>

View File

@@ -0,0 +1,34 @@
@use 'sass:list';
#guide {
margin: 1em
}
#legend {
display: flex;
justify-self: center;
* {
margin: 2px;
}
gap: 3ch;
}
.circle {
&::before {
border-radius: 7.5%;
width: 2.5ch;
height: 2.5ch;
display: inline-block;
content: "";
vertical-align: middle;
margin-right: 3px;
}
$list: (red, yellow, green);
@for $n from 1 through 3 {
&:nth-of-type(#{$n})::before {
background-color: nth($list, $n);
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AttendenceSummaryComponent } from './attendence-summary.component';
describe('AttendenceSummaryComponent', () => {
let component: AttendenceSummaryComponent;
let fixture: ComponentFixture<AttendenceSummaryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AttendenceSummaryComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AttendenceSummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { ToolbarService } from '../../toolbar.service';
import { Router, ActivatedRoute } from '@angular/router';
import { MatTableDataSource } from '@angular/material/table';
import { AdminCommService } from '../../admin-comm.service';
@Component({
selector: 'app-attendence-summary',
templateUrl: './attendence-summary.component.html',
styleUrl: './attendence-summary.component.scss'
})
export class AttendenceSummaryComponent implements OnInit {
data: MatTableDataSource<{room: string, hours: string[]}> = new MatTableDataSource<{room: string, hours: string[]}>();
collumns = ['room', 'hours']
constructor (private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private ac: AdminCommService) {
this.toolbar.comp = this
this.toolbar.menu = [
{check: true, title: "Ocenianie", fn: "goBack", icon: "arrow_back"}
]
}
ngOnInit(): void {
this.ac.clean.attendence.getSummary().subscribe(v => {
this.data.data = v
})
}
goBack() {
this.router.navigate(['../'], {relativeTo: this.route})
}
}

View File

@@ -0,0 +1 @@
<span [ngStyle]="style()">{{value}}</span>

View File

@@ -0,0 +1,8 @@
:host {
display: inline;
}
span {
padding: 2px;
border-radius: 7.5%;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HourDisplayComponent } from './hour-display.component';
describe('HourDisplayComponent', () => {
let component: HourDisplayComponent;
let fixture: ComponentFixture<HourDisplayComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HourDisplayComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HourDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import { Component, Input } from '@angular/core';
import * as moment from 'moment';
@Component({
selector: 'app-hour-display',
templateUrl: './hour-display.component.html',
styleUrl: './hour-display.component.scss'
})
export class HourDisplayComponent {
@Input() value = "";
style () {
if (/(0+[0-9]|1[0-9]|2[0-3]):(0+[0-9]|[1-5][0-9])/g.test(this.value)) {
var diff = moment(this.value, 'HH:mm').diff(moment(), 'minutes');
if (diff > 30) {
return { "background-color": "red" }
} else if (diff > 0) {
return { "background-color": "yellow", "color": "black"}
} else {
return { "background-color": "green"}
}
} else {
return { "color": "gray"}
}
}
}

View File

@@ -0,0 +1,18 @@
<span mat-dialog-title>Obecność w {{room}}</span>
<mat-dialog-content>
<form [formGroup]="this.form">
<div formArrayName="users">
@for (item of users.controls; track i; let i = $index) {
<div [formGroupName]="i">
<mat-checkbox formControlName="att" #cb>
<span control="label"></span>:
<input type="time" formControlName="hour" (input)="cb.writeValue(true)">
</mat-checkbox>
</div>
}
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="save()">Zapisz</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AttendenceComponent } from './attendence.component';
describe('AttendenceComponent', () => {
let component: AttendenceComponent;
let fixture: ComponentFixture<AttendenceComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AttendenceComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AttendenceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,46 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AdminCommService } from '../../admin-comm.service';
@Component({
selector: 'app-attendence',
templateUrl: './attendence.component.html',
styleUrl: './attendence.component.scss'
})
export class AttendenceComponent implements OnInit {
constructor (private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: {room: string}, public dialogRef: MatDialogRef<AttendenceComponent>, private ac: AdminCommService) {}
ngOnInit(): void {
this.room = this.data.room
this.ac.clean.attendence.getUsers(this.room).subscribe(query => {
query.users.forEach(v => {
var att = query.attendence ? query.attendence.find(z => z.id == v._id) : false
this.users.push(this.fb.group({
id: v._id,
label: `${v.fname} ${v.surname}`,
att: this.fb.control(att),
hour: this.fb.control(att ? att.hour : ""),
}))
})
})
}
save() {
this.dialogRef.close({
room: this.room,
...this.form.value
})
}
room: string = "";
form: FormGroup = this.fb.group({
users: this.fb.array([])
})
get users() {
return this.form.get('users') as FormArray
}
}

View File

@@ -3,10 +3,12 @@
<form [formGroup]="form">
<p>Czystość pokoju {{room}} na dzień {{date.format("dddd")}}</p>
<p>Ocena: {{grade}}</p>
<button mat-flat-button (click)="downloadData()">Anuluj</button>
<!-- <button mat-flat-button (click)="calculate()">Oblicz</button> -->
<button mat-flat-button (click)="save()">Zapisz</button>
<button mat-raised-button color="warn" (click)="remove()" *ngIf="id">Usuń</button>
<div id="buttons">
<button mat-mini-fab (click)="downloadData()" color="accent"><mat-icon>cancel</mat-icon></button>
<button mat-mini-fab (click)="attendence()" color="accent"><mat-icon>overview</mat-icon></button>
<button mat-mini-fab (click)="save()" color="accent"><mat-icon>save</mat-icon></button>
<button mat-mini-fab color="warn" (click)="remove()" *ngIf="id"><mat-icon>delete</mat-icon></button>
</div>
<div *ngFor="let item of things.controls; let i = index" formArrayName="things" id="things">
<div formGroupName="{{i}}">
<mat-checkbox formControlName="cb" #cb>

View File

@@ -2,3 +2,9 @@ div#things {
display: flex;
flex-direction: column;
}
div#buttons {
* {
margin: 0 2px 0 2px;
}
}

View File

@@ -6,6 +6,8 @@ import { weekendFilter } from 'src/app/fd.da';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToolbarService } from '../toolbar.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { AttendenceComponent } from './attendence/attendence.component';
@Component({
selector: 'app-grades',
@@ -42,12 +44,13 @@ export class GradesComponent implements OnInit, OnDestroy {
})
}
constructor(private ac: AdminCommService, private fb: FormBuilder, private sb: MatSnackBar, private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute) {
constructor(private ac: AdminCommService, private fb: FormBuilder, private sb: MatSnackBar, private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private dialog: MatDialog) {
this.date = moment.utc().startOf('day')
if (!this.filter(this.date)) this.date.isoWeekday(8);
this.toolbar.comp = this
this.toolbar.menu = [
{ title: "Podsumowanie", check: true, fn: "summary", icon: "analytics" }
{ title: "Podsumowanie", check: true, fn: "summary", icon: "analytics" },
{ title: "Obecność", check: true, fn: "attendenceSummary", icon: "overview"}
]
this.form.valueChanges.subscribe((v) => {
this.calculate()
@@ -67,6 +70,10 @@ export class GradesComponent implements OnInit, OnDestroy {
this.router.navigate(["summary"], { relativeTo: this.route })
}
attendenceSummary() {
this.router.navigate(["attendenceSummary"], {relativeTo: this.route})
}
ngOnInit(): void {
this.ac.clean.getConfig().subscribe((s) => {
this.rooms = s.rooms
@@ -139,6 +146,7 @@ export class GradesComponent implements OnInit, OnDestroy {
}
this.ac.clean.postClean(obj).subscribe((s) => {
this.sb.open("Zapisano!", undefined, { duration: 1500 })
this.downloadData()
})
}
@@ -154,4 +162,23 @@ export class GradesComponent implements OnInit, OnDestroy {
this.room = value
this.downloadData()
}
attendence() {
this.dialog.open(AttendenceComponent, {data: {room: this.room}}).afterClosed().subscribe((v: {room: string, users: {att: boolean, id: string, hour: string}[]}) => {
let x: {room: string, users: {id: string, hour?: string}[]} = {
room: v.room,
users: []
}
v.users.forEach(i => {
if (i.att && i.hour) {
x.users.push({id: i.id, hour: i.hour})
}
})
this.ac.clean.attendence.postAttendence(x.room, x.users).subscribe((s) => {
if (s.status == 200) {
this.sb.open("Zapisano obecność!", undefined, {duration: 1500})
}
})
})
}
}

View File

@@ -1,10 +1,11 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ToolbarService } from '../../toolbar.service';
import { ActivatedRoute, Router } from '@angular/router';
import { AdminCommService } from '../../admin-comm.service';
import * as moment from 'moment';
import { MatTableDataSource } from '@angular/material/table';
import { FormBuilder } from '@angular/forms';
import { MatSort } from '@angular/material/sort';
@Component({
selector: 'app-summary',
@@ -21,6 +22,10 @@ export class SummaryComponent implements OnInit, OnDestroy {
end: this.fb.control(moment.utc().endOf('day'))
})
@ViewChild(MatSort, {static: false}) set content(sort: MatSort) {
this.data.sort = sort
}
constructor (private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private ac: AdminCommService, private fb: FormBuilder) {
this.toolbar.comp = this
this.toolbar.menu = [

View File

@@ -7,6 +7,8 @@ import { AdminCommService } from '../admin-comm.service';
import { FormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { NewKeyComponent } from './new-key/new-key.component';
import { catchError, throwError } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-admin-key',
@@ -33,7 +35,7 @@ export class AdminKeyComponent implements AfterViewInit, OnInit {
loading = true
@ViewChild(MatPaginator) paginator!: MatPaginator
constructor (private ac: AdminCommService, private dialog: MatDialog) {
constructor (private ac: AdminCommService, private dialog: MatDialog, private sb: MatSnackBar) {
this.filters = []
}
@@ -71,7 +73,12 @@ export class AdminKeyComponent implements AfterViewInit, OnInit {
new() {
this.dialog.open(NewKeyComponent).afterClosed().subscribe(v => {
if (v) {
this.ac.keys.postKey(v.room, v.user).subscribe((s) => {
this.ac.keys.postKey(v.room, v.user).pipe(catchError((err,caught)=>{
if (err.status == 404) {
this.sb.open("Nie znaleziono użytkownika", undefined, {duration: 2500})
}
return throwError(() => new Error(err.message))
})).subscribe((s) => {
if (s.status == 201) {
this.fetchData()
}

View File

@@ -11,13 +11,13 @@ import { NewsEditComponent } from './admin-view/news-edit/news-edit.component';
import { AccountMgmtComponent } from './admin-view/account-mgmt/account-mgmt.component';
import { MenuNewComponent } from './admin-view/menu-new/menu-new.component';
import { adminGuard } from './admin.guard';
// import { NotificationsComponent } from './admin-view/notifications/notifications.component';
import { GroupsComponent } from './admin-view/groups/groups.component';
import { StartComponent } from './app-view/start/start.component';
import { AdminKeyComponent } from './admin-view/key/key.component';
import { GradesComponent } from './admin-view/grades/grades.component';
import { SummaryComponent } from './admin-view/grades/summary/summary.component';
import { SettingsComponent } from './admin-view/settings/settings.component';
import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component';
const routes: Routes = [
{path: "", redirectTo: "login", pathMatch: "full"},
@@ -37,7 +37,8 @@ const routes: Routes = [
{path: "keys", title: "Klucze", component: AdminKeyComponent},
{path: "grades", children: [
{path: "", pathMatch: "full", title: "Oceny", component: GradesComponent},
{path: "summary", title: "Podsumowanie ocen", component: SummaryComponent}
{path: "summary", title: "Podsumowanie ocen", component: SummaryComponent},
{path: "attendenceSummary", title: "Obecność", component: AttendenceSummaryComponent}
]},
{path: "settings", title: "Ustawienia", component: SettingsComponent}
]}

View File

@@ -74,6 +74,9 @@ import { FieldEditorComponent } from './commonComponents/field-editor/field-edit
import { A11yModule } from '@angular/cdk/a11y';
import { PortalModule } from '@angular/cdk/portal';
import { MatAutocompleteModule } from "@angular/material/autocomplete";
import { AttendenceComponent } from './admin-view/grades/attendence/attendence.component';
import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component';
import { HourDisplayComponent } from './admin-view/grades/attendence-summary/hour-display/hour-display.component';
@NgModule({
declarations: [
@@ -113,6 +116,9 @@ import { MatAutocompleteModule } from "@angular/material/autocomplete";
SettingsComponent,
MenuAddComponent,
FieldEditorComponent,
AttendenceComponent,
AttendenceSummaryComponent,
HourDisplayComponent,
],
imports: [
BrowserModule,