8 Commits
1.2.0 ... main

Author SHA1 Message Date
fa126e02de actions: correct browser
All checks were successful
IPWA Tests / Run Angular tests (22) (push) Successful in 5m12s
2025-06-19 10:55:03 +02:00
a65baa06fd actions: add karma config
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 4m3s
2025-06-19 10:45:17 +02:00
da8c21eb0d actions:test
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 2m16s
2025-06-19 10:29:11 +02:00
2e42f65d88 actions: add chrome path
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 4m3s
2025-06-19 10:18:20 +02:00
33eef9d305 actions: deps
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 12m13s
2025-06-18 22:33:55 +02:00
6a145b224c actions: setup chrome
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 2m34s
2025-06-18 20:37:17 +02:00
913ccd0bf1 actions: update matrix
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 2m3s
2025-06-18 19:40:28 +02:00
271ba89704 actions: Add tests
Some checks failed
IPWA Tests / Run Angular tests (22) (push) Failing after 1m6s
2025-06-18 19:38:12 +02:00
234 changed files with 12407 additions and 10305 deletions

View File

@@ -0,0 +1,42 @@
name: IPWA Tests
run-name: ${{ gitea.actor }} is testing out IPWA
on: [push]
jobs:
Run Angular tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22]
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Cache node modules
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{matrix.node-version}}
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Setup Chrome
uses: browser-actions/setup-chrome@v1.7.3
id: setup-chrome
with:
install-dependencies: true
- name: Install node dependencies
run: npm ci
- name: Echo chrome path
run: |
echo "$CHROME_BIN"
env:
CHROME_BIN: ${{steps.setup-chrome.outputs.chrome-path}}
- name: Run tests
run: npm run ci
env:
CHROME_BIN: ${{steps.setup-chrome.outputs.chrome-path}}

View File

@@ -1,8 +0,0 @@
angular.json
compose.yml
*.md
tsconfig*.json
*.html
.vscode
package*.json
ngsw-config.json

View File

@@ -1,6 +0,0 @@
{
"arrowParens": "avoid",
"semi": false,
"trailingComma": "es5",
"bracketSpacing": true
}

View File

@@ -8,7 +8,7 @@ FROM httpd:alpine AS runtime
RUN apk add --no-cache certbot certbot-apache
COPY httpd.conf /usr/local/apache2/conf/httpd.conf
COPY cli.ini /etc/letsencrypt/cli.ini
COPY --from=build /build/dist/ipwa/browser /usr/local/apache2/htdocs/ipwa
COPY --from=build /build/dist /usr/local/apache2/htdocs/
COPY <<EOF /usr/local/apache2/htdocs/ipwa/.htaccess
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]

View File

@@ -16,15 +16,13 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": {
"base": "dist/ipwa"
},
"outputPath": "dist/ipwa",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js",
"@angular/localize/init"
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
@@ -37,11 +35,11 @@
"src/styles.scss"
],
"scripts": [],
"serviceWorker": "ngsw-config.json",
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json",
"allowedCommonJsDependencies": [
"moment"
],
"browser": "src/main.ts"
]
},
"configurations": {
"production": {
@@ -60,7 +58,9 @@
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
@@ -72,7 +72,9 @@
]
},
"swDevelopment": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
@@ -97,7 +99,7 @@
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "ipwa:build:production"
@@ -108,25 +110,25 @@
},
"options": {
"servePath": "/ipwa/",
"open": false
"open": true
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "ipwa:build"
}
},
"test": {
"builder": "@angular/build:karma",
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing",
"@angular/localize/init"
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
@@ -139,36 +141,7 @@
"scripts": []
}
}
},
"i18n": {
"sourceLocale": "pl"
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

45
karma.conf.js Normal file
View File

@@ -0,0 +1,45 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/ipwa'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
browsers: ['Chrome'],
restartOnFileChange: true,
customLaunchers: {
ChromeHeadlessCustom: {
base: "ChromeHeadless",
flags: ["--no-sandbox", "--disable-gpu"],
},
},
});
};

12497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ipwa",
"version": "1.2.0",
"version": "1.1.1",
"license": "GPL-3.0-or-later",
"scripts": {
"ng": "ng",
@@ -8,42 +8,39 @@
"build": "ng build -c production",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"ci": "ng test --no-watch --no-progress --browsers=ChromiumHeadless"
"ci": "ng test --no-watch --no-progress --browsers=ChromeHeadlessCustom"
},
"private": true,
"dependencies": {
"@angular/animations": "^20.0.2",
"@angular/cdk": "^20.0.2",
"@angular/cli": "^20.0.1",
"@angular/common": "^20.0.2",
"@angular/compiler": "^20.0.2",
"@angular/core": "^20.0.2",
"@angular/forms": "^20.0.2",
"@angular/material": "^20.0.2",
"@angular/material-luxon-adapter": "^20.0.2",
"@angular/platform-browser": "^20.0.2",
"@angular/platform-browser-dynamic": "^20.0.2",
"@angular/router": "^20.0.2",
"@angular/service-worker": "^20.0.2",
"luxon": "^3.6.1",
"@angular/animations": "^17.3.2",
"@angular/cdk": "^17.3.2",
"@angular/cli": "^17.3.2",
"@angular/common": "^17.3.2",
"@angular/compiler": "^17.3.2",
"@angular/core": "^17.3.2",
"@angular/forms": "^17.3.2",
"@angular/material": "^17.3.2",
"@angular/material-moment-adapter": "^17.3.2",
"@angular/platform-browser": "^17.3.2",
"@angular/platform-browser-dynamic": "^17.3.2",
"@angular/router": "^17.3.2",
"@angular/service-worker": "^17.3.2",
"marked": "^12.0.1",
"moment": "^2.29.4",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.1"
"zone.js": "~0.14.4"
},
"devDependencies": {
"@angular/build": "^20.0.1",
"@angular/compiler-cli": "^20.0.2",
"@angular/localize": "^20.0.2",
"@angular-devkit/build-angular": "^17.3.2",
"@angular/compiler-cli": "^17.3.2",
"@types/jasmine": "~4.3.0",
"@types/luxon": "^3.6.2",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "3.5.3",
"typescript": "~5.8.3"
"typescript": "~5.4.3"
}
}

View File

@@ -1,39 +1,35 @@
<div id="upper-bar">
<mat-form-field subscriptSizing="dynamic">
<mat-label>Wyszukaj</mat-label>
<input matInput (keyup)="filter($event)">
</mat-form-field>
<button mat-icon-button (click)="openUserCard()"><mat-icon>add</mat-icon></button>
<mat-form-field>
<mat-label>Wyszukaj</mat-label>
<input matInput (keyup)="filter($event)">
</mat-form-field>
<button mat-icon-button (click)="openUserCard()"><mat-icon>add</mat-icon></button>
</div>
<div class="mainc">
@if (ac.state() != STATE.LOADED) {
<app-load-shade [state]="ac.state()" [error]="ac.error()" (refresh)="ac.refresh()"/>
}
<table mat-table [dataSource]="users">
<mat-spinner *ngIf="loading"></mat-spinner>
<table mat-table [dataSource]="users">
<div matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Imię</th>
<td mat-cell *matCellDef="let element">{{element.fname}}</td>
<th mat-header-cell *matHeaderCellDef>Imię</th>
<td mat-cell *matCellDef="let element">{{element.fname}}</td>
</div>
<div matColumnDef="surname">
<th mat-header-cell *matHeaderCellDef>Nazwisko</th>
<td mat-cell *matCellDef="let element">{{element.surname}}</td>
<th mat-header-cell *matHeaderCellDef>Nazwisko</th>
<td mat-cell *matCellDef="let element">{{element.surname}}</td>
</div>
<div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>Pokój</th>
<td mat-cell *matCellDef="let element">{{element.room}}</td>
<th mat-header-cell *matHeaderCellDef>Pokój</th>
<td mat-cell *matCellDef="let element">{{element.room}}</td>
</div>
<div matColumnDef="uname">
<th mat-header-cell *matHeaderCellDef>Nazwa użytkownika</th>
<td mat-cell *matCellDef="let element">{{element.uname}}</td>
<th mat-header-cell *matHeaderCellDef>Nazwa użytkownika</th>
<td mat-cell *matCellDef="let element">{{element.uname}}</td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Karta użytkownika</th>
<td mat-cell *matCellDef="let element">
<button mat-mini-fab (click)="openUserCard(element._id)"><mat-icon>manage_accounts</mat-icon></button>
</td>
<th mat-header-cell *matHeaderCellDef>Karta użytkownika</th>
<td mat-cell *matCellDef="let element">
<button mat-mini-fab (click)="openUserCard(element._id)"><mat-icon>manage_accounts</mat-icon></button>
</td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let row; columns: collumns"></tr>
</table>
<mat-paginator pageSize="9" [pageSizeOptions]="[9, 15, 20, 50, 160]"></mat-paginator>
</div>
</table>
<mat-paginator pageSize="9" [pageSizeOptions]="[9, 15, 20, 50, 160]"></mat-paginator>

View File

@@ -1,30 +1,23 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
}
.mainc {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
height: 100%;
}
mat-paginator {
margin-top: auto;
margin-top: auto;
}
mat-form-field {
flex-grow: 1;
flex-grow: 1;
}
#upper-bar {
display: flex;
display: flex;
}
button[mat-icon-button] {
margin-left: 4pt;
margin-right: 4pt;
margin-top: 4pt;
}
margin-left: 4pt;
margin-right: 4pt;
margin-top: 4pt;
}

View File

@@ -1,54 +1,52 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountMgmtComponent } from './account-mgmt.component'
import { MatDialogModule } from '@angular/material/dialog'
import { MatSnackBarModule } from '@angular/material/snack-bar'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatIconModule } from '@angular/material/icon'
import { MatPaginatorModule } from '@angular/material/paginator'
import { MatTableModule } from '@angular/material/table'
import { MatInputModule } from '@angular/material/input'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'
import { AccountMgmtService } from './account-mgmt.service'
import { LoadShadeComponent } from 'src/app/commonComponents/load-shade/load-shade.component'
import { of } from 'rxjs'
import { signal } from '@angular/core'
import { STATE } from 'src/app/types/state'
import { AccountMgmtComponent } from './account-mgmt.component';
import { AdminCommService } from '../admin-comm.service';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { of } from 'rxjs';
import { MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
describe('AccountMgmtComponent', () => {
let component: AccountMgmtComponent
let fixture: ComponentFixture<AccountMgmtComponent>
let component: AccountMgmtComponent;
let fixture: ComponentFixture<AccountMgmtComponent>;
let acMock
beforeEach(async () => {
acMock = {
accs: of([]),
state: signal(STATE.NOT_LOADED),
refresh: jasmine.createSpy('getAccs'),
error: signal(undefined)
accs: {
getAccs: jasmine.createSpy("getAccs").and.returnValue(of())
}
}
await TestBed.configureTestingModule({
declarations: [AccountMgmtComponent, LoadShadeComponent],
providers: [{ provide: AccountMgmtService, useValue: acMock }],
imports: [
MatDialogModule,
MatSnackBarModule,
MatFormFieldModule,
MatIconModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
BrowserAnimationsModule,
MatProgressSpinnerModule,
declarations: [AccountMgmtComponent],
providers: [
{provide: AdminCommService, useValue: acMock}
],
}).compileComponents()
fixture = TestBed.createComponent(AccountMgmtComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [
MatDialogModule,
MatSnackBarModule,
MatFormFieldModule,
MatIconModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
BrowserAnimationsModule,
MatProgressSpinnerModule
]
}).compileComponents();
fixture = TestBed.createComponent(AccountMgmtComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,78 +1,63 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core'
import { MatDialog } from '@angular/material/dialog'
import { MatTableDataSource } from '@angular/material/table'
import { MatPaginator } from '@angular/material/paginator'
import { UserEditComponent } from './user-edit/user-edit.component'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { Group } from 'src/app/types/group'
import { User } from 'src/app/admin-view/account-mgmt/account.model'
import { AccountMgmtService } from './account-mgmt.service'
import { STATE } from 'src/app/types/state'
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { AdminCommService } from '../admin-comm.service';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { UserEditComponent } from './user-edit/user-edit.component';
import { LocalStorageService } from 'src/app/services/local-storage.service';
import { Group } from 'src/app/types/group';
import User from 'src/app/types/user';
@Component({
selector: 'app-account-mgmt',
templateUrl: './account-mgmt.component.html',
styleUrls: ['./account-mgmt.component.scss'],
standalone: false,
styleUrls: ['./account-mgmt.component.scss']
})
export class AccountMgmtComponent implements AfterViewInit {
export class AccountMgmtComponent implements OnInit, AfterViewInit {
protected groups: Group[] = []
users: MatTableDataSource<User>
users: MatTableDataSource<Omit<User, "pass">>
loading = false
@ViewChild(MatPaginator) paginator!: MatPaginator
constructor(
protected ac: AccountMgmtService,
private dialog: MatDialog,
protected readonly ls: LocalStorageService
) {
this.users = new MatTableDataSource<User>()
this.users.filterPredicate = (
data: Record<string, any>,
filter: string
): boolean => {
const dataStr = Object.keys(data)
.reduce((curr: string, key: string) => {
if (['_id', 'admin', 'groups', '__v', 'locked'].find(v => v == key)) {
return curr + ''
}
return curr + data[key] + '⫂'
}, '')
.toLowerCase()
constructor(readonly ac:AdminCommService, private dialog: MatDialog, private sb: MatSnackBar, protected readonly ls: LocalStorageService) {
this.users = new MatTableDataSource<Omit<User, "pass">>();
this.users.filterPredicate = (data: Record<string, any>, filter: string): boolean => {
const dataStr = Object.keys(data).reduce((curr: string, key: string) => {
if (["_id", "admin", "groups", "__v", "locked"].find(v => v == key)) {
return curr + ''
}
return curr + data[key] + '⫂'
}, '').toLowerCase()
const filternew = filter.trim().toLowerCase()
return dataStr.indexOf(filternew) != -1
}
this.ac.refresh()
this.ac.accs.subscribe(d => {
this.users.data = d
})
}
protected get STATE(): typeof STATE {
return STATE
}
ngAfterViewInit() {
this.users.paginator = this.paginator
}
ngOnInit() {
this.loading = true
this.ac.accs.getAccs().subscribe((data)=>{
this.loading = false
this.users.data = data.users
this.groups = data.groups
})
}
filter(event: Event) {
const value = (event.target as HTMLInputElement).value
this.users.filter = value.toLowerCase().trim()
}
openUserCard(id?: string) {
this.dialog
.open<
UserEditComponent,
UserEditComponent.InputData,
UserEditComponent.ReturnData
>(UserEditComponent, {
data: { id: id, type: id ? 'edit' : 'new', groups: this.groups },
})
.afterClosed()
.subscribe(r => {
if (r) this.ac.refresh()
})
this.dialog.open<UserEditComponent, UserEditComponent.InputData, UserEditComponent.ReturnData>(UserEditComponent, {data: {id: id, type: id ? "edit" : "new", groups: this.groups}}).afterClosed().subscribe(r => {
if (r) this.ngOnInit()
})
}
collumns = ['name', 'surname', 'uname', 'actions']

View File

@@ -1,96 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { AccountMgmtService } from './account-mgmt.service';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { environment } from 'src/environments/environment';
import { firstValueFrom, skip } from 'rxjs';
describe('AccountMgmtService', () => {
let service: AccountMgmtService;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
httpTesting = TestBed.inject(HttpTestingController);
service = TestBed.inject(AccountMgmtService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should get user accounts', () => {
service.refresh()
const req = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request to load all users")
expect(req.request.method).toBe("GET")
req.flush([])
httpTesting.verify()
})
describe('create user', () => {
xit('should create a user account and refresh list', () => {
const test_user = {
uname: "test",
groups: []
}
service.postAcc(test_user).subscribe(v => {
expect(v).toEqual(jasmine.objectContaining(test_user))
})
const req = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request new user")
expect(req.request.method).toBe("POST")
req.flush({
...test_user,
_id: "test_id"
})
const req2 = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request to load all users")
expect(req2.request.method).toBe("GET")
// service.accs.pipe(skip(1)).subscribe(v => {
// expect(v).toContain(createdUser)
// })
req2.flush([
{
...test_user,
_id: "test_id"
}
])
httpTesting.verify()
})
})
describe("delete user", () => {
it('should refresh accounts and not to contain deleted user', async () => {
service.deleteAcc("test").subscribe()
const req = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs/test", "Request delete user")
expect(req.request.method).toBe("DELETE")
req.flush({ status: 200 })
const req2 = httpTesting.expectOne(environment.apiEndpoint + "/admin/accs", "Request to load all users")
expect(req2.request.method).toBe("GET")
service.accs.pipe(skip(1)).subscribe(v => {
expect(v).not.toContain(jasmine.objectContaining({ _id: "test" }))
})
req2.flush([])
httpTesting.verify()
})
})
});

View File

@@ -1,110 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, signal } from '@angular/core';
import { BehaviorSubject, catchError, map, of, tap } from 'rxjs';
import { STATE } from 'src/app/types/state';
import { Status } from 'src/app/types/status';
import { User, UserAPI } from 'src/app/admin-view/account-mgmt/account.model';
import { environment } from 'src/environments/environment';
import { DateTime } from 'luxon';
@Injectable({
providedIn: 'root'
})
export class AccountMgmtService {
constructor(private http: HttpClient) { }
private _accs = new BehaviorSubject<User[]>([])
public readonly accs = this._accs.asObservable()
private _state = signal(STATE.NOT_LOADED);
public readonly state = this._state.asReadonly();
private _error = signal<string | undefined>(undefined);
public readonly error = this._error.asReadonly();
private _selAcc = new BehaviorSubject<User | null>(null)
public readonly selAcc = this._selAcc.asObservable()
public refresh() {
this.getAccs()
}
private getAccs() {
this._state.set(STATE.PENDING)
this.http.get
<UserAPI[]>
(environment.apiEndpoint + `/admin/accs`, { withCredentials: true })
.pipe(
catchError((err: Error) => {
this._state.set(STATE.ERROR)
this._error.set(err.message)
return of()
}),
map<UserAPI[], User[]>(v => {
return v.map(i => ({
...i,
regDate: DateTime.fromISO(i.regDate)
}))
})
).subscribe(v => {
this._error.set(undefined)
this._accs.next(v ?? [])
this._state.set(STATE.LOADED)
})
}
selectAccount(acc: User) {
this._selAcc.next(acc)
}
//#region legacy
postAcc(item: Omit<User, "_id" | "regDate">) {
return this.http.post<User>(
environment.apiEndpoint + `/admin/accs`,
item,
{ withCredentials: true }
).pipe(tap(v => {
if (v instanceof Array) this.refresh()
}))
}
putAcc(id: string, update: Partial<User>) {
return this.http.put<Status>(
environment.apiEndpoint + `/admin/accs/${id}`,
update,
{ withCredentials: true }
)
}
resetPass(id: string) {
return this.http.patch<Status>(
environment.apiEndpoint + `/admin/accs/${id}/reset`,
undefined,
{ withCredentials: true }
)
}
deleteAcc(id: string) {
return this.http.delete<Status>(
environment.apiEndpoint + `/admin/accs/${id}`,
{ withCredentials: true }
)
.pipe(tap(v => {
if (v.status == 200) this.refresh()
}))
}
getUser(id: string) {
return this.http.get<
Omit<User, 'regDate'> & { lockout: boolean; regDate: string }
>(environment.apiEndpoint + `/admin/accs/${id}`, {
withCredentials: true,
})
}
clearLockout(id: string) {
return this.http.delete<Status>(
environment.apiEndpoint + `/admin/accs/${id}/lockout`,
{ withCredentials: true }
)
}
//#endregion
}

View File

@@ -1,16 +0,0 @@
import { DateTime } from 'luxon'
export interface User {
_id: string
uname: string
room?: string
admin?: string[]
locked?: boolean
fname?: string
surname?: string
groups: string[]
regDate: DateTime
defaultPage?: string
}
export type UserAPI = Omit<User, "regDate"> & {regDate: "string"}

View File

@@ -1,23 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserDeleteComponent } from './user-delete.component'
import { MatDialogModule } from '@angular/material/dialog'
import { UserDeleteComponent } from './user-delete.component';
import { MatDialogModule } from '@angular/material/dialog';
describe('UserDeleteComponent', () => {
let component: UserDeleteComponent
let fixture: ComponentFixture<UserDeleteComponent>
let component: UserDeleteComponent;
let fixture: ComponentFixture<UserDeleteComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserDeleteComponent],
imports: [MatDialogModule],
})
fixture = TestBed.createComponent(UserDeleteComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [MatDialogModule]
});
fixture = TestBed.createComponent(UserDeleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core'
import { Component } from '@angular/core';
@Component({
selector: 'app-user-delete',
templateUrl: './user-delete.component.html',
styleUrls: ['./user-delete.component.scss'],
standalone: false,
styleUrls: ['./user-delete.component.scss']
})
export class UserDeleteComponent {}
export class UserDeleteComponent {
}

View File

@@ -1,85 +1,67 @@
<h1 mat-dialog-title>Karta użytkownika</h1>
<mat-dialog-content>
<form [formGroup]="form">
<div>
<mat-form-field appearance="outline" color="accent">
<mat-label>Imię</mat-label>
<input type="text" matInput formControlName="fname">
</mat-form-field>
<mat-form-field appearance="outline" color="accent">
<mat-label>Nazwisko</mat-label>
<input type="text" matInput formControlName="surname">
</mat-form-field>
<mat-form-field appearance="outline" color="accent">
<mat-label>Pokój</mat-label>
<input type="text" matInput formControlName="room">
</mat-form-field>
<mat-form-field appearance="outline" color="accent">
<mat-label>Grupy</mat-label>
<mat-select multiple formControlName="groups">
@for (item of adsyn.groups; track $index) {
<mat-option [value]="item._id">{{item.name}}</mat-option>
}
</mat-select>
</mat-form-field>
@if (data.type == 'edit') {
<span>Data rejestracji:<br>{{regDate?.toFormat('D')}}</span>
}
</div>
<div>
<mat-form-field appearance="outline" color="accent">
<mat-label>Nazwa użytkownika</mat-label>
<input type="text" matInput required formControlName="uname">
</mat-form-field>
@if (data.type == "edit") {
<button mat-stroked-button color="accent" (click)="resetPass()">Resetuj hasło</button>
@if (locked) {
<button mat-stroked-button color="warn" (click)="toggleLock(false)"><mat-icon>lock</mat-icon>Blokada ręczna</button>
} @else {
<button mat-stroked-button color="accent" (click)="toggleLock(true)">Zablokuj konto</button>
}
@if (lockout) {
<button mat-stroked-button color="warn" (click)="disableLockout()"><mat-icon>lock_clock</mat-icon>Auto-Blokada</button>
} @else {
<button mat-stroked-button disabled>Auto-Blokada nieczynna</button>
}
@if (ls.permChecker("accs")) {
<mat-form-field color="accent">
<mat-label>Uprawnienia</mat-label>
<mat-select multiple formControlName="flags">
@if (ls.capCheck(1)) {
<mat-option value="news">Wiadomości</mat-option>
}
@if (ls.capCheck(2)) {
<mat-option value="menu">Jadłospis</mat-option>
}
@if (ls.capCheck(4)) {
<mat-option value="notif">Powiadomienia</mat-option>
}
@if (ls.capCheck(8)) {
<mat-option value="groups">Grupy</mat-option>
}
<mat-option value="accs">Konta</mat-option>
@if (ls.capCheck(32)) {
<mat-option value="keys">Klucze</mat-option>
}
@if (ls.capCheck(16)) {
<mat-option value="grades">Czystość</mat-option>
}
</mat-select>
</mat-form-field>
}
}
</div>
</form>
<form [formGroup]="form">
<div>
<mat-form-field appearance="outline" color="accent">
<mat-label>Imię</mat-label>
<input type="text" matInput formControlName="fname">
</mat-form-field>
<mat-form-field appearance="outline" color="accent">
<mat-label>Nazwisko</mat-label>
<input type="text" matInput formControlName="surname">
</mat-form-field>
<mat-form-field appearance="outline" color="accent">
<mat-label>Pokój</mat-label>
<input type="text" matInput formControlName="room">
</mat-form-field>
<mat-form-field appearance="outline" color="accent">
<mat-label>Grupy</mat-label>
<mat-select multiple formControlName="groups">
@for (item of groups; track $index) {
<mat-option [value]="item._id">{{item.name}}</mat-option>
}
</mat-select>
</mat-form-field>
<span *ngIf="data.type == 'edit'">Data rejestracji:<br>{{regDate?.format('DD.MM.YYYY')}}</span>
</div>
<div>
<mat-form-field appearance="outline" color="accent">
<mat-label>Nazwa użytkownika</mat-label>
<input type="text" matInput required formControlName="uname">
</mat-form-field>
@if (data.type == "edit") {
<button mat-stroked-button color="accent" (click)="resetPass()">Resetuj hasło</button>
@if (locked) {
<button mat-stroked-button color="warn" (click)="toggleLock(false)"><mat-icon>lock</mat-icon>Blokada ręczna</button>
} @else {
<button mat-stroked-button color="accent" (click)="toggleLock(true)">Zablokuj konto</button>
}
@if (lockout) {
<button mat-stroked-button color="warn" (click)="disableLockout()"><mat-icon>lock_clock</mat-icon>Auto-Blokada</button>
} @else {
<button mat-stroked-button disabled>Auto-Blokada nieczynna</button>
}
<mat-form-field *ngIf="ls.permChecker(32)" color="accent">
<mat-label>Uprawnienia</mat-label>
<mat-select multiple formControlName="flags">
<mat-option [value]="1" *ngIf="ls.capCheck(1)">Wiadomości</mat-option>
<mat-option [value]="2" *ngIf="ls.capCheck(2)">Jadłospis</mat-option>
<mat-option [value]="4" *ngIf="ls.capCheck(4)">Powiadomienia</mat-option>
<mat-option [value]="8" *ngIf="ls.capCheck(8)">Grupy</mat-option>
<mat-option [value]="16">Konta</mat-option>
<mat-option [value]="64" *ngIf="ls.capCheck(32)">Klucze</mat-option>
<mat-option [value]="128" *ngIf="ls.capCheck(16)">Czystość</mat-option>
</mat-select>
</mat-form-field>
}
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions>
@if (data.type == "edit") {
<button mat-stroked-button color="warn" style="margin-right: auto;" (click)="delete()">Usuń konto</button>
}
<button mat-stroked-button mat-dialog-close>Zamknij</button>
<button mat-flat-button color="accent" (click)="submit()">Zapisz</button>
@if (loading) {
<mat-spinner diameter="32" color="accent"></mat-spinner>
}
</mat-dialog-actions>
@if (data.type == "edit") {
<button mat-stroked-button color="warn" style="margin-right: auto;" (click)="delete()">Usuń konto</button>
}
<button mat-stroked-button mat-dialog-close>Zamknij</button>
<button mat-flat-button color="accent" (click)="submit()">Zapisz</button>
<mat-spinner diameter="32" color="accent" *ngIf="loading"></mat-spinner>
</mat-dialog-actions>

View File

@@ -1,32 +1,32 @@
:host {
padding: 8pt;
display: block;
padding: 8pt;
display: block;
}
form {
margin-top: 1ch !important;
display: flex;
grid-auto-flow: column;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
column-gap: 3ch;
div {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(5, 1fr);
margin-top: 1ch !important;
display: flex;
grid-auto-flow: column;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
button {
align-self: stretch;
justify-self: stretch;
height: auto;
margin-bottom: 1lh;
justify-content: center;
column-gap: 3ch;
div {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(5, 1fr);
align-items: center;
button {
align-self: stretch;
justify-self: stretch;
height: auto;
margin-bottom: 1lh;
}
}
}
}
mat-dialog-actions {
display: flex;
justify-content: flex-end;
}
display: flex;
justify-content: flex-end;
}

View File

@@ -1,24 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserEditComponent } from './user-edit.component'
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { ReactiveFormsModule } from '@angular/forms'
import { MatInputModule } from '@angular/material/input'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { MatSelectModule } from '@angular/material/select'
import { UserEditComponent } from './user-edit.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AdminCommService } from '../../admin-comm.service';
import { forwardRef } from '@angular/core';
import { MatSelectModule } from '@angular/material/select';
xdescribe('UserEditComponent', () => {
let component: UserEditComponent
let fixture: ComponentFixture<UserEditComponent>
describe('UserEditComponent', () => {
let component: UserEditComponent;
let fixture: ComponentFixture<UserEditComponent>;
let acMock
beforeEach(async () => {
acMock = {}
acMock = {
}
await TestBed.configureTestingModule({
declarations: [UserEditComponent],
imports: [
@@ -27,20 +27,20 @@ xdescribe('UserEditComponent', () => {
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule,
MatSelectModule,
MatSelectModule
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { groups: [] } },
// { provide: AdminCommService, useValue: acMock },
],
}).compileComponents()
fixture = TestBed.createComponent(UserEditComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
{ provide: AdminCommService, useValue: acMock },
]
}).compileComponents();
fixture = TestBed.createComponent(UserEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,196 +1,174 @@
import { Component, Inject } from '@angular/core'
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog'
import { FormControl, FormGroup } from '@angular/forms'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { Group } from 'src/app/types/group'
import { UserDeleteComponent } from '../user-delete/user-delete.component'
import { MatSnackBar } from '@angular/material/snack-bar'
import { UserResetComponent } from '../user-reset/user-reset.component'
import { catchError, throwError } from 'rxjs'
import { DateTime } from 'luxon'
import { AccountMgmtService } from '../account-mgmt.service'
import { AdminSyncService } from '../../admin-sync.service'
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormGroup } from '@angular/forms';
import { LocalStorageService } from 'src/app/services/local-storage.service';
import { Group } from 'src/app/types/group';
import { AdminCommService } from '../../admin-comm.service';
import { UserDeleteComponent } from '../user-delete/user-delete.component';
import { MatSnackBar } from '@angular/material/snack-bar';
import { UserResetComponent } from '../user-reset/user-reset.component';
import { catchError, throwError } from 'rxjs';
import { Moment } from 'moment';
import * as moment from 'moment';
export namespace UserEditComponent {
export type InputData = { type: 'new' | 'edit'; id?: string; groups: Group[] }
export type InputData = {type: "new" | "edit", id?: string, groups: Group[]}
export type ReturnData = true | undefined
}
@Component({
selector: 'app-user-edit',
templateUrl: './user-edit.component.html',
styleUrls: ['./user-edit.component.scss'],
standalone: false,
styleUrls: ['./user-edit.component.scss']
})
export class UserEditComponent {
lockout = false
locked = false
loading = false
export class UserEditComponent {
lockout = false;
locked = false;
loading = false;
form: FormGroup = new FormGroup({
fname: new FormControl<string>(''),
surname: new FormControl<string>(''),
room: new FormControl<string>(''),
uname: new FormControl<string>(''),
fname: new FormControl<string>(""),
surname: new FormControl<string>(""),
room: new FormControl<string>(""),
uname: new FormControl<string>(""),
groups: new FormControl<Array<string>>([]),
flags: new FormControl<Array<string>>([]),
flags: new FormControl<Array<number>>([]),
})
groups: Group[]
id?: string
regDate?: DateTime
constructor(
public dialogRef: MatDialogRef<UserEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: UserEditComponent.InputData,
readonly ls: LocalStorageService,
readonly acu: AccountMgmtService,
regDate?: Moment;
constructor (
public dialogRef: MatDialogRef<UserEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: UserEditComponent.InputData,
readonly ls: LocalStorageService,
readonly acu: AdminCommService,
private dialog: MatDialog,
private sb: MatSnackBar,
protected adsyn: AdminSyncService
private sb: MatSnackBar
) {
if (data.type == 'edit') {
this.groups = data.groups
if (data.type == "edit") {
this.id = data.id
this.acu.getUser(data.id!).subscribe(r => {
this.regDate = DateTime.fromISO(r.regDate)
this.acu.accs.getUser(data.id!).subscribe((r) => {
this.regDate = moment(r.regDate)
var flags: Array<number> = []
if (r.admin) {
if ((r.admin & 1) == 1) flags.push(1)
if ((r.admin & 2) == 2) flags.push(2)
if ((r.admin & 4) == 4) flags.push(4)
if ((r.admin & 8) == 8) flags.push(8)
if ((r.admin & 16) == 16) flags.push(16)
if ((r.admin & 32) == 32) flags.push(32)
if ((r.admin & 64) == 64) flags.push(64)
if ((r.admin & 128) == 128) flags.push(128)
}
this.locked = r.locked ? true : false
this.lockout = r.lockout
this.form.get('fname')?.setValue(r.fname)
this.form.get('surname')?.setValue(r.surname)
this.form.get('room')?.setValue(r.room)
this.form.get('uname')?.setValue(r.uname)
this.form.get('groups')?.setValue(r.groups)
this.form.get('flags')?.setValue(r.admin)
this.form.get("fname")?.setValue(r.fname)
this.form.get("surname")?.setValue(r.surname)
this.form.get("room")?.setValue(r.room)
this.form.get("uname")?.setValue(r.uname)
this.form.get("groups")?.setValue(r.groups)
this.form.get("flags")?.setValue(flags)
})
}
}
protected submit() {
this.loading = true
if (this.data.type == 'edit') {
this.acu
.putAcc(this.id!, this.getForm())
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(data => {
if (data.status == 200) {
this.sb.open('Użytkownik został zmodyfikowany.', undefined, {
duration: 2500,
})
this.dialogRef.close(true)
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
this.loading = false
}
})
if (this.data.type == "edit") {
this.acu.accs.putAcc(this.id!, this.getForm()).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 200) {
this.sb.open("Użytkownik został zmodyfikowany.", undefined, {duration: 2500})
this.dialogRef.close(true)
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
this.loading = false
}
})
} else {
this.acu
.postAcc(this.getForm())
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(data => {
if (data) {
this.sb.open('Użytkownik został utworzony.', undefined, {
duration: 2500,
})
this.dialogRef.close(true)
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
this.loading = false
}
})
this.acu.accs.postAcc(this.getForm()).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 201) {
this.sb.open("Użytkownik został utworzony.", undefined, {duration: 2500})
this.dialogRef.close(true)
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
this.loading = false
}
})
}
}
protected disableLockout() {
this.loading = true
this.acu
.clearLockout(this.id!)
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(s => {
if (s.status == 200) {
this.loading = false
this.lockout = false
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
this.loading = false
}
})
this.acu.accs.clearLockout(this.id!).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((s) => {
if (s.status == 200) {
this.loading = false
this.lockout = false
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
this.loading = false
}
})
}
protected getForm() {
protected getForm() {
return {
fname: this.form.get('fname')?.value,
surname: this.form.get('surname')?.value,
room: this.form.get('room')?.value,
uname: this.form.get('uname')?.value,
groups: this.form.get('groups')?.value,
admin: (() => {
var value = this.form.get('flags')?.value
flags: (() => {
var value = this.form.get('flags')?.value.reduce((a: number,b: number)=>a+b,0)
if (this.ls.capCheck(32)) {
return value
} else {
return undefined
}
})(),
})()
}
}
protected delete() {
this.dialog
.open(UserDeleteComponent)
.afterClosed()
.subscribe(reply => {
if (reply) {
this.acu.deleteAcc(this.id!).subscribe(res => {
if (res.status == 200) {
this.sb.open('Użytkownik został usunięty.', undefined, {
duration: 2500,
})
this.dialogRef.close()
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
console.error(res)
}
})
}
})
this.dialog.open(UserDeleteComponent).afterClosed().subscribe(reply => {
if (reply) {
this.acu.accs.deleteAcc(this.id!).subscribe((res) => {
if (res.status == 200) {
this.sb.open("Użytkownik został usunięty.", undefined, {duration: 2500})
this.dialogRef.close()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
console.error(res);
}
})
}
})
}
protected resetPass() {
this.loading = true
this.dialog
.open(UserResetComponent)
.afterClosed()
.subscribe(res => {
if (res == true) {
this.acu.resetPass(this.id!).subscribe(patch => {
if (patch.status == 200) {
this.sb.open('Hasło zostało zresetowane', undefined, {
duration: 2500,
})
this.loading = false
}
})
}
})
this.dialog.open(UserResetComponent).afterClosed().subscribe((res) => {
if (res == true) {
this.acu.accs.resetPass(this.id!).subscribe((patch)=>{
if (patch.status == 200) {
this.sb.open("Hasło zostało zresetowane", undefined, {duration: 2500})
this.loading = false
}
})
}
})
}
protected toggleLock(state: boolean) {
this.acu.putAcc(this.id!, { locked: state }).subscribe(res => {
this.acu.accs.putAcc(this.id!, {locked: state}).subscribe((res) => {
if (res.status == 200) {
this.locked = state
}

View File

@@ -1,23 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserResetComponent } from './user-reset.component'
import { MatDialogModule } from '@angular/material/dialog'
import { UserResetComponent } from './user-reset.component';
import { MatDialogModule } from '@angular/material/dialog';
describe('UserResetComponent', () => {
let component: UserResetComponent
let fixture: ComponentFixture<UserResetComponent>
let component: UserResetComponent;
let fixture: ComponentFixture<UserResetComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserResetComponent],
imports: [MatDialogModule],
})
fixture = TestBed.createComponent(UserResetComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [
MatDialogModule
]
});
fixture = TestBed.createComponent(UserResetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core'
import { Component } from '@angular/core';
@Component({
selector: 'app-user-reset',
templateUrl: './user-reset.component.html',
styleUrls: ['./user-reset.component.scss'],
standalone: false,
styleUrls: ['./user-reset.component.scss']
})
export class UserResetComponent {}
export class UserResetComponent {
}

View File

@@ -0,0 +1,24 @@
import { TestBed } from '@angular/core/testing';
import { AdminCommService } from './admin-comm.service';
import { HttpClient } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('AdminCommService', () => {
let service: AdminCommService;
let httpClient: HttpClient
let httpTestingController: HttpTestingController
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
service = TestBed.inject(AdminCommService);
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,282 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Moment } from 'moment';
import { environment } from 'src/environments/environment';
import { Menu } from '../types/menu';
import { Status } from '../types/status';
import { Group } from '../types/group';
import { map, of } from 'rxjs';
import { Notification } from '../types/notification';
import { News } from '../types/news';
import { AKey } from '../types/key';
import * as moment from 'moment';
import { IUSettings } from './settings/settings.component';
import User from '../types/user';
@Injectable({
providedIn: 'root'
})
export class AdminCommService {
constructor(private http: HttpClient) { }
//#region Menu
menu = {
getMenu: (start?: Moment | null, end?: Moment | null) => {
if (start && end) {
const body = {start: start.toString(), end: end.toString()}
return this.http.get<Menu[]>(environment.apiEndpoint+"/admin/menu", {withCredentials: true, params: body})
}
return
},
getOpts: () => {
return this.http.get<any>(environment.apiEndpoint+`/admin/menu/opts`, {withCredentials: true})
},
postMenu: (file: File) => {
if (file) {
const formData = new FormData();
formData.append("menu", file)
return this.http.post<Status>(environment.apiEndpoint+"/admin/menu/upload", formData, {withCredentials: true})
}
return
},
editSn: (id: string, content: Menu['sn']) => {
return this.putMenu(id, {sn: content})
},
editOb: (id: string, content: Menu['ob']) => {
return this.putMenu(id, {ob: content})
},
editKol: (id: string, content: Menu['kol']) => {
return this.putMenu(id, {kol: content})
},
editTitle: (id: string, content: Menu['dayTitle']) => {
return this.putMenu(id, {dayTitle: content})
},
print: (start?: Moment | null, end?: Moment | null) => {
if (start && end) {
const body = {start: start.toString(), end: end.toString()}
return this.http.get(environment.apiEndpoint+"/admin/menu/print", {withCredentials: true, params: body, responseType: "text"})
}
return
},
stat: (day: Moment, m: "ob" | "kol") => {
return this.http.get<{y: number, n: number}>(environment.apiEndpoint+`/admin/menu/${day.toISOString()}/votes/${m}`, {withCredentials: true})
},
new: {
single: (day: Moment) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/menu/${day.toISOString()}`, null, {withCredentials: true})
},
range: (start: Moment, count: number) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/menu/${start.toISOString()}/${count}/`, null, {withCredentials: true})
}
},
rm: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/menu/${id}`, {withCredentials: true})
}
}
private putMenu(id: string, update: Partial<Menu>) {
return this.http.put<Status>(environment.apiEndpoint+`/admin/menu/${id}`, update, {withCredentials: true})
}
//#endregion
//#region News
news = {
getNews: () => {
return this.http.get<News[]>(environment.apiEndpoint+`/admin/news`, {withCredentials: true})
},
postNews: (title: string, content: string) => {
return this.http.post<any>(environment.apiEndpoint+`/admin/news`, {title: title, content: content}, {withCredentials: true})
},
deleteNews: (id: string) => {
return this.http.delete<any>(environment.apiEndpoint+`/admin/news/${id}`, {withCredentials: true})
},
toggleNews: (id: string, inverter: boolean) => {
return this.putNews(id,{visible: !inverter})
},
togglePin: (id: string, inverter: boolean) => {
return this.putNews(id,{pinned: !inverter})
},
updateNews: (id: string, title: string, content: string) => {
return this.putNews(id,{title: title, content: content, date: Date.now})
}
}
private putNews(id: string, update: object) {
return this.http.put<any>(environment.apiEndpoint+`/admin/news/${id}`, update, {withCredentials: true})
}
//#endregion
//#region amgmt
accs = {
getAccs: () => {
return this.http.get<{
users: Omit<User, "pass">[],
groups: Group[]
}>(environment.apiEndpoint+`/admin/accs`, {withCredentials: true})
},
postAcc: (item: any) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/accs`, item, {withCredentials: true})
},
putAcc: (id: string, update: Partial<User>) => {
return this.http.put<Status>(environment.apiEndpoint+`/admin/accs/${id}`, update, {withCredentials: true})
},
resetPass: (id: string) => {
return this.http.patch<Status>(environment.apiEndpoint+`/admin/accs/${id}/reset`, {}, {withCredentials: true})
},
deleteAcc: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/accs/${id}`, {withCredentials: true})
},
getUser: (id: string) => {
return this.http.get<Omit<User, "pass"> & {lockout: boolean}>(environment.apiEndpoint+`/admin/accs/${id}`, {withCredentials: true})
},
clearLockout: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/accs/${id}/lockout`, {withCredentials: true})
}
}
//#endregion
//#region Groups
groups = {
getGroups: () => {
return this.http.get<Group[]>(environment.apiEndpoint+`/admin/groups`, {withCredentials: true})
},
newGroup: (name: string) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/groups`, {name: name}, {withCredentials: true})
},
editName: (id: string, name: string) => {
return this.putGroups(id, {name: name.trim()})
},
remove: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/groups/${id}`, {withCredentials: true})
}
}
private putGroups(id: string, update: Partial<Group>) {
return this.http.put<Status>(environment.apiEndpoint+`/admin/groups/${id}`, update, {withCredentials: true})
}
//#endregion
//#region Notif
notif = {
send: (n: Notification) => {
return this.http.post<{sent: number, possible: number}>(environment.apiEndpoint+"/admin/notif/send", n, {withCredentials: true})
},
getGroups: () => {
return this.http.get<Group[]>(environment.apiEndpoint+"/admin/notif/groups", {withCredentials: true})
},
outbox: {
getSent: () => {
return this.http.get<{_id: string, sentDate: moment.Moment, title: string}[]>(environment.apiEndpoint+"/admin/notif/outbox", {withCredentials: true})
},
getBody: (id: string) => {
return this.http.get(environment.apiEndpoint+`/admin/notif/outbox/${id}/message`, {withCredentials: true, responseType: "text"})
},
getRcpts: (id: string) => {
return this.http.get<{_id: string, uname: string, room?: string, fname?: string, surname?: string}[]>(environment.apiEndpoint+`/admin/notif/outbox/${id}/rcpts`, {withCredentials: true})
}
}
}
//#endregion
//#region Keys
keys = {
getKeys: () => {
return this.http.get<AKey[]>(environment.apiEndpoint+`/admin/keys`, {withCredentials: true}).pipe(map((v) => {
return v.map((r) => {
r.borrow = moment(r.borrow)
if (r.tb) r.tb = moment(r.tb)
return r
})
}))
},
avalKeys: () => {
return this.http.get<string[]>(environment.apiEndpoint+`/admin/keys/available`, {withCredentials: true})
},
postKey: (room: string, uname: string) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/keys/`, {room: room, whom: uname}, {withCredentials: true})
},
returnKey: (id: string) => {
return this.putKeys(id, {tb: moment.utc()})
}
}
private putKeys(id: string, update: Partial<AKey>) {
return this.http.put<Status>(environment.apiEndpoint+`/admin/keys/${id}`, update, {withCredentials: true})
}
//#endregion
//#region Clean
clean = {
getConfig: () => {
return this.http.get<{rooms: string[], things: string[]}>(environment.apiEndpoint+`/admin/clean/config`, {withCredentials: true})
},
getClean: (date: moment.Moment, room: string) => {
return this.http.get<{_id: string, date: string, grade: number, gradeDate: string, notes: {label: string, weight: number}[], room: string, tips: string} | null>(environment.apiEndpoint+`/admin/clean/${date.toISOString()}/${room}`, {withCredentials: true})
},
postClean: (obj: Object) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/clean/`, obj, {withCredentials: true})
},
delete: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/clean/${id}`, {withCredentials: true})
},
summary: {
getSummary: (start: moment.Moment, end: moment.Moment) => {
return this.http.get<{room: string, 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?: {auto: {id: string, hour?: string}[], notes: string}}>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, {withCredentials: true})
},
postAttendence: (room: string, attendence: {auto: {id: string, hour?: string}[], notes: string}) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, attendence, {withCredentials: true})
},
getSummary: () => {
return this.http.get<{room: string, hours: string[], notes: string, auto: boolean}[]>(environment.apiEndpoint+`/admin/clean/attendenceSummary`, {withCredentials: true})
},
deleteRoom: (room: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, {withCredentials: true})
}
}
}
//#endregion
//#region Settings
settings = {
getAll: () => {
return this.http.get<IUSettings>(environment.apiEndpoint+`/admin/settings/`, {withCredentials: true})
},
post: (settings: IUSettings) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/settings/`, settings, {withCredentials: true})
},
reload: () => {
return this.http.get<Status>(environment.apiEndpoint+`/admin/settings/reload/`, {withCredentials: true})
}
}
//#endregion
//#region misc
userFilter = (query: string) => {
return this.http.get<any[]>(environment.apiEndpoint+`/admin/usearch`, {params: {q: query}, withCredentials: true})
}
//#endregion
}

View File

@@ -1,23 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { AdminSyncService } from './admin-sync.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('AdminSyncService', () => {
let service: AdminSyncService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(AdminSyncService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,27 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Group } from '../types/group';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class AdminSyncService {
constructor(private http: HttpClient) { }
private _data: any
private sync() {
this.http.get(environment.apiEndpoint + `/admin/sync`, { withCredentials: true }).subscribe(v => {
this._data = v
})
}
public get groups(): Group[] {
var groups = this._data?.groups
if (!groups) this.sync()
return groups
}
}

View File

@@ -1,15 +1,14 @@
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
mat-sidenav,
mat-toolbar {
padding: 8pt;
mat-sidenav, mat-toolbar {
padding: 8pt
}
mat-sidenav-container {
flex-grow: 1;
}
flex-grow: 1;
}

View File

@@ -1,45 +1,34 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminViewComponent } from './admin-view.component'
import { MatToolbarModule } from '@angular/material/toolbar'
import { MatIconModule } from '@angular/material/icon'
import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { MatListModule } from '@angular/material/list'
import { RouterModule } from '@angular/router'
import { Component, Input } from '@angular/core'
import { AdminViewComponent } from './admin-view.component';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatListModule } from '@angular/material/list';
import { RouterModule } from '@angular/router';
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-toolbar',
template: '',
standalone: false,
})
@Component({selector: "app-toolbar", template: ''})
class ToolbarMock {
@Input() drawer!: MatDrawer
@Input() drawer!: MatDrawer;
}
describe('AdminViewComponent', () => {
let component: AdminViewComponent
let fixture: ComponentFixture<AdminViewComponent>
let component: AdminViewComponent;
let fixture: ComponentFixture<AdminViewComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AdminViewComponent, ToolbarMock],
imports: [
MatToolbarModule,
MatIconModule,
MatSidenavModule,
BrowserAnimationsModule,
MatListModule,
RouterModule.forRoot([]),
],
})
fixture = TestBed.createComponent(AdminViewComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [MatToolbarModule, MatIconModule, MatSidenavModule, BrowserAnimationsModule, MatListModule, RouterModule.forRoot([])]
});
fixture = TestBed.createComponent(AdminViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,78 +1,29 @@
import { Component } from '@angular/core'
import { Router } from '@angular/router'
import { LocalStorageService } from '../services/local-storage.service'
import { Link } from '../types/link'
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LocalStorageService } from '../services/local-storage.service';
import { Link } from '../types/link';
@Component({
selector: 'app-admin-view',
templateUrl: './admin-view.component.html',
styleUrls: ['./admin-view.component.scss'],
standalone: false,
styleUrls: ['./admin-view.component.scss']
})
export class AdminViewComponent {
private readonly _LINKS: Link[] = [
{
title: 'Wiadomości',
icon: 'newspaper',
href: 'news',
enabled: this.ls.permChecker("news") && this.ls.capCheck(1),
},
{
title: 'Jadłospis',
icon: 'restaurant_menu',
href: 'menu',
enabled: this.ls.permChecker("menu") && this.ls.capCheck(2),
},
{
title: 'Wysyłanie powiadomień',
icon: 'notifications',
href: 'notifications',
enabled: this.ls.permChecker("notif") && this.ls.capCheck(4),
},
{
title: 'Grupy',
icon: 'groups',
href: 'groups',
enabled: this.ls.permChecker("groups") && this.ls.capCheck(8),
},
{
title: 'Zarządzanie kontami',
icon: 'manage_accounts',
href: 'accounts',
enabled: this.ls.permChecker("accs"),
},
{
title: 'Klucze',
icon: 'key',
href: 'keys',
enabled: this.ls.permChecker("keys") && this.ls.capCheck(32),
},
{
title: 'Czystość',
icon: 'cleaning_services',
href: 'grades',
enabled: this.ls.permChecker("grades") && this.ls.capCheck(16),
},
{
title: 'Frekwencja',
icon: 'checklist',
href: 'attendence',
enabled: false,
},
{
title: 'Ustawienia',
icon: 'settings_applications',
href: 'settings',
enabled: this.ls.permChecker("super"),
},
]
{ title: "Wiadomości", icon: "newspaper", href: "news", enabled: this.ls.permChecker(1) && this.ls.capCheck(1) },
{ title: "Jadłospis", icon: "restaurant_menu", href: "menu", enabled: this.ls.permChecker(2) && this.ls.capCheck(2) },
{ title: "Wysyłanie powiadomień", icon: "notifications", href: "notifications", enabled: this.ls.permChecker(4) && this.ls.capCheck(4) },
{ title: "Grupy", icon: "groups", href: "groups", enabled: this.ls.permChecker(8) && this.ls.capCheck(8) },
{ title: "Zarządzanie kontami", icon: "manage_accounts", href: "accounts", enabled: this.ls.permChecker(16) },
{ title: "Klucze", icon: "key", href: "keys", enabled: this.ls.permChecker(64) && this.ls.capCheck(32) },
{ title: "Czystość", icon: "cleaning_services", href: "grades", enabled: this.ls.permChecker(128) && this.ls.capCheck(16) },
{ title: "Frekwencja", icon: "checklist", href: "attendence", enabled: false },
{ title: "Ustawienia", icon: "settings_applications", href: "settings", enabled: this.ls.permChecker(32) },
];
public get LINKS(): Link[] {
return this._LINKS.filter(v => v.enabled)
return this._LINKS.filter(v => v.enabled);
}
constructor(
readonly router: Router,
readonly ls: LocalStorageService
) {}
constructor(readonly router: Router, readonly ls: LocalStorageService) { }
goNormal() {
this.router.navigateByUrl('app')
}

View File

@@ -1,29 +1,25 @@
<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>
<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">@for (i of item.hours.sort().reverse(); track i; let isLast = $last) {
<span><app-hour-display [value]="i"></app-hour-display>{{ isLast ? '' : ', '}}</span>
}<span>{{item.notes}}</span></td>
<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><span>{{item.notes}}</span></td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Usuń</th>
<td mat-cell *matCellDef="let item">@if (!item.auto) {
<button mat-mini-fab color="warn" (click)="delete(item.room)"><mat-icon>delete</mat-icon></button>
}</td>
<th mat-header-cell *matHeaderCellDef>Usuń</th>
<td mat-cell *matCellDef="let item"><button mat-mini-fab color="warn" (click)="delete(item.room)" *ngIf="!item.auto"><mat-icon>delete</mat-icon></button></td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let rowData; columns: collumns"></tr>
</table>
</table>

View File

@@ -1,34 +1,34 @@
@use "sass:list";
@use 'sass:list';
#guide {
margin: 1em;
margin: 1em
}
#legend {
display: flex;
justify-self: center;
gap: 3ch;
* {
margin: 2px;
}
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: list.nth($list, $n);
&::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

@@ -1,35 +1,42 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AttendenceSummaryComponent } from './attendence-summary.component'
import { RouterModule } from '@angular/router'
import { of } from 'rxjs'
import { MatTableModule } from '@angular/material/table'
import { AttendenceSummaryComponent } from './attendence-summary.component';
import { RouterModule } from '@angular/router';
import { AdminCommService } from '../../admin-comm.service';
import { of } from 'rxjs';
import { MatTableModule } from '@angular/material/table';
xdescribe('AttendenceSummaryComponent', () => {
let component: AttendenceSummaryComponent
let fixture: ComponentFixture<AttendenceSummaryComponent>
describe('AttendenceSummaryComponent', () => {
let component: AttendenceSummaryComponent;
let fixture: ComponentFixture<AttendenceSummaryComponent>;
let acMock
beforeEach(async () => {
acMock = {
clean: {
attendence: {
getSummary: jasmine.createSpy('getSummary').and.returnValue(of()),
},
},
getSummary: jasmine.createSpy("getSummary").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({
declarations: [AttendenceSummaryComponent],
imports: [RouterModule.forRoot([]), MatTableModule],
// providers: [{ provide: AdminCommService, useValue: acMock }],
}).compileComponents()
fixture = TestBed.createComponent(AttendenceSummaryComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [
RouterModule.forRoot([]),
MatTableModule
],
providers: [
{provide: AdminCommService, useValue: acMock}
]
})
.compileComponents();
fixture = TestBed.createComponent(AttendenceSummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,54 +1,39 @@
import { Component, OnInit } from '@angular/core'
import { ToolbarService } from '../../toolbar/toolbar.service'
import { Router, ActivatedRoute } from '@angular/router'
import { MatTableDataSource } from '@angular/material/table'
import { GradesService } from '../grades.service'
import { Component, OnInit } from '@angular/core';
import { ToolbarService } from '../../toolbar/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',
standalone: false,
styleUrl: './attendence-summary.component.scss'
})
export class AttendenceSummaryComponent implements OnInit {
data: MatTableDataSource<{
room: string
hours: string[]
notes: string
auto: boolean
}> = new MatTableDataSource<{
room: string
hours: string[]
notes: string
auto: boolean
}>()
data: MatTableDataSource<{room: string, hours: string[], notes: string, auto: boolean}> = new MatTableDataSource<{room: string, hours: string[], notes: string, auto: boolean}>();
collumns = ['room', 'hours', 'actions']
constructor(
private toolbar: ToolbarService,
private router: Router,
private route: ActivatedRoute,
private ac: GradesService
) {
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' },
{check: true, title: "Ocenianie", fn: "goBack", icon: "arrow_back"}
]
}
delete(room: string) {
this.ac.attendence.deleteRoom(room).subscribe(() => {
this.ac.clean.attendence.deleteRoom(room).subscribe(() => {
this.ngOnInit()
})
}
ngOnInit(): void {
this.ac.attendence.getSummary().subscribe(v => {
this.ac.clean.attendence.getSummary().subscribe(v => {
this.data.data = v
})
}
goBack() {
this.router.navigate(['../'], { relativeTo: this.route })
this.router.navigate(['../'], {relativeTo: this.route})
}
}

View File

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

View File

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

View File

@@ -1,27 +1,26 @@
import { Component, Input } from '@angular/core'
import { DateTime } from 'luxon'
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',
standalone: false,
styleUrl: './hour-display.component.scss'
})
export class HourDisplayComponent {
@Input() value = ''
style() {
@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 = DateTime.fromFormat(this.value, 'HH:mm').diffNow('minutes')
if (diff.as('minutes') > 30) {
return { 'background-color': 'red' }
} else if (diff.as('minutes') > 0) {
return { 'background-color': 'yellow', color: 'black' }
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' }
return { "background-color": "green"}
}
} else {
return { color: 'gray' }
return { "color": "gray"}
}
}
}

View File

@@ -1,35 +1,32 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AttendenceComponent } from './attendence.component'
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { of } from 'rxjs'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MatInputModule } from '@angular/material/input'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { AttendenceComponent } from './attendence.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { AdminCommService } from '../../admin-comm.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
xdescribe('AttendenceComponent', () => {
let component: AttendenceComponent
let fixture: ComponentFixture<AttendenceComponent>
describe('AttendenceComponent', () => {
let component: AttendenceComponent;
let fixture: ComponentFixture<AttendenceComponent>;
beforeEach(async () => {
const acMock = {
clean: {
attendence: {
getUsers: jasmine.createSpy('getUsers').and.returnValue(of()),
},
},
getUsers: jasmine.createSpy("getUsers").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({
declarations: [AttendenceComponent],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} },
// { provide: AdminCommService, useValue: acMock },
{provide: MAT_DIALOG_DATA, useValue: {}},
{provide: MatDialogRef, useValue: {}},
{provide: AdminCommService, useValue: acMock}
],
imports: [
MatDialogModule,
@@ -37,16 +34,17 @@ xdescribe('AttendenceComponent', () => {
FormsModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule,
],
}).compileComponents()
fixture = TestBed.createComponent(AttendenceComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
NoopAnimationsModule
]
})
.compileComponents();
fixture = TestBed.createComponent(AttendenceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,37 +1,28 @@
import { Component, Inject, OnInit } from '@angular/core'
import { FormArray, FormBuilder, FormGroup } from '@angular/forms'
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'
import { GradesService } from '../grades.service'
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',
standalone: false,
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: GradesService
) {}
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.attendence.getUsers(this.room).subscribe(query => {
this.ac.clean.attendence.getUsers(this.room).subscribe(query => {
query.users.forEach(v => {
var att = query.attendence
? query.attendence.auto.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 : ''),
})
)
var att = query.attendence ? query.attendence.auto.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 : ""),
}))
})
this.form.get('notes')?.setValue(query.attendence?.notes)
})
@@ -40,15 +31,15 @@ export class AttendenceComponent implements OnInit {
save() {
this.dialogRef.close({
room: this.room,
...this.form.value,
...this.form.value
})
}
room: string = ''
room: string = "";
form: FormGroup = this.fb.group({
users: this.fb.array([]),
notes: this.fb.control(''),
notes: this.fb.control(""),
})
get users() {

View File

@@ -1,34 +1,28 @@
<app-date-selector [(date)]="date" [filter]="filter" (dateChange)="downloadData()"></app-date-selector>
<app-room-chooser [rooms]="rooms" (room)="roomNumber($event)"/>
<form [formGroup]="form">
<p>Czystość pokoju {{room}} na {{date().toFormat("cccc, D")}}</p>
<p>Ocena: <span [appGradeColor]="grade">{{grade}}</span></p>
<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>
@if (id) {
<button mat-mini-fab color="warn" (click)="remove()"><mat-icon>delete</mat-icon></button>
}
</div>
@for (item of things.controls; track item; let i = $index) {
<div formArrayName="things" id="things">
<div formGroupName="{{i}}">
<mat-checkbox formControlName="cb" #cb>
<span control="label"></span>
@if (cb.checked) {
<span>
<button mat-icon-button (click)="group.sub(i)"><mat-icon>remove</mat-icon></button>
<span control="weight"></span>
<button mat-icon-button (click)="group.add(i)"><mat-icon>add</mat-icon></button>
</span>
}
</mat-checkbox>
</div>
<p>Czystość pokoju {{room}} na dzień {{date.format("dddd")}}</p>
<p>Ocena: {{grade}}</p>
<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>
}
<mat-form-field style="width: 100%;">
<mat-label>Dodatkowe uwagi</mat-label>
<textarea matNativeControl cdkTextareaAutosize formControlName="tips"></textarea>
</mat-form-field>
</form>
<div *ngFor="let item of things.controls; let i = index" formArrayName="things" id="things">
<div formGroupName="{{i}}">
<mat-checkbox formControlName="cb" #cb>
<span control="label"></span>
<span *ngIf="cb.checked">
<button mat-icon-button (click)="group.sub(i)"><mat-icon>remove</mat-icon></button>
<span control="weight"></span>
<button mat-icon-button (click)="group.add(i)"><mat-icon>add</mat-icon></button>
</span>
</mat-checkbox>
</div>
</div>
<mat-form-field style="width: 100%;">
<mat-label>Dodatkowe uwagi</mat-label>
<textarea matNativeControl cdkTextareaAutosize formControlName="tips"></textarea>
</mat-form-field>
</form>

View File

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

View File

@@ -1,51 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GradesComponent } from './grades.component'
import { RouterModule } from '@angular/router'
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { MatIconModule } from '@angular/material/icon'
import { MatFormFieldModule } from '@angular/material/form-field'
import { of } from 'rxjs'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MatInputModule } from '@angular/material/input'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { DateTime } from 'luxon'
import { GradesComponent } from './grades.component';
import { AdminCommService } from '../admin-comm.service';
import { RouterModule } from '@angular/router';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import * as moment from 'moment';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: 'app-date-selector',
template: '',
standalone: false,
})
@Component({selector: "app-date-selector", template: ''})
class DateSelectorStub {
@Input() date: string = DateTime.now().toISODate()
@Output() dateChange = new EventEmitter<string>()
@Input() filter: (date: DateTime | null) => boolean = () => true
@Input() date: moment.Moment = moment.utc().startOf('day');
@Output() dateChange = new EventEmitter<moment.Moment>();
@Input() filter: (date: moment.Moment | null) => boolean = () => true
}
@Component({
selector: 'app-room-chooser',
template: '',
standalone: false,
})
@Component({selector: "app-room-chooser", template: ''})
class RoomSelectorStub {
@Input() rooms: string[] = []
@Output() room: EventEmitter<string> = new EventEmitter<string>()
@Output() room: EventEmitter<string> = new EventEmitter<string>();
}
xdescribe('GradesComponent', () => {
let component: GradesComponent
let fixture: ComponentFixture<GradesComponent>
describe('GradesComponent', () => {
let component: GradesComponent;
let fixture: ComponentFixture<GradesComponent>;
let acMock
beforeEach(async () => {
acMock = {
clean: {
getConfig: jasmine.createSpy('getConfig').and.returnValue(of()),
},
getConfig: jasmine.createSpy("getConfig").and.returnValue(of())
}
}
await TestBed.configureTestingModule({
declarations: [GradesComponent, DateSelectorStub, RoomSelectorStub],
// providers: [{ provide: AdminCommService, useValue: acMock }],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
RouterModule.forRoot([]),
MatIconModule,
@@ -53,16 +48,17 @@ xdescribe('GradesComponent', () => {
FormsModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule,
],
}).compileComponents()
fixture = TestBed.createComponent(GradesComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
NoopAnimationsModule
]
})
.compileComponents();
fixture = TestBed.createComponent(GradesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,46 +1,39 @@
import { Component, OnDestroy, OnInit, signal } from '@angular/core'
import { FormArray, FormBuilder } from '@angular/forms'
import { filterLook, weekendFilter } from 'src/app/util'
import { MatSnackBar } from '@angular/material/snack-bar'
import { ToolbarService } from '../toolbar/toolbar.service'
import { ActivatedRoute, Router } from '@angular/router'
import { MatDialog } from '@angular/material/dialog'
import { AttendenceComponent } from './attendence/attendence.component'
import { DateTime } from 'luxon'
import { GradesService } from './grades.service'
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AdminCommService } from '../admin-comm.service';
import * as moment from 'moment';
import { FormArray, FormBuilder } from '@angular/forms';
import { weekendFilter } from 'src/app/fd.da';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToolbarService } from '../toolbar/toolbar.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { AttendenceComponent } from './attendence/attendence.component';
@Component({
selector: 'app-grades',
templateUrl: './grades.component.html',
styleUrl: './grades.component.scss',
standalone: false,
styleUrl: './grades.component.scss'
})
export class GradesComponent implements OnInit, OnDestroy {
rooms!: string[]
room: string = '0'
room: string = "0";
date: moment.Moment;
grade: number = 6
gradeDate?: DateTime
gradeDate?: moment.Moment;
id?: string
filter = weekendFilter
date = signal<DateTime>(filterLook(this.filter, "behind", DateTime.now(), 7)!)
get notes(): { label: string; weight: number }[] {
var th = this.things.value as {
cb: boolean
label: string
weight: number
}[]
return th
.filter(v => v.cb)
.map(v => {
return { ...v, cb: undefined }
})
get notes(): { label: string, weight: number }[] {
var th = this.things.value as { cb: boolean, label: string, weight: number }[]
return th.filter((v) => v.cb).map((v) => {
return { ...v, cb: undefined }
})
}
set notes(value: { label: string; weight: number }[]) {
set notes(value: { label: string, weight: number }[]) {
var things = this.things.controls
things.forEach(v => {
var thing = value.find(s => s.label == v.get('label')?.value)
things.forEach((v) => {
var thing = value.find((s) => s.label == v.get('label')?.value)
if (thing) {
v.get('cb')?.setValue(true)
v.get('weight')?.setValue(thing.weight)
@@ -51,34 +44,22 @@ export class GradesComponent implements OnInit, OnDestroy {
})
}
constructor(
private ac: GradesService,
private fb: FormBuilder,
private sb: MatSnackBar,
private toolbar: ToolbarService,
private router: Router,
private route: ActivatedRoute,
private dialog: MatDialog
) {
// if (!this.filter(this.date)) this.date.isoWeekday(8);
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: 'Pokoje do sprawdzenia',
check: true,
fn: 'attendenceSummary',
icon: 'overview',
},
{ title: 'Podsumowanie', check: true, fn: 'summary', icon: 'analytics' },
{ title: "Pokoje do sprawdzenia", check: true, fn: "attendenceSummary", icon: "overview"},
{ title: "Podsumowanie", check: true, fn: "summary", icon: "analytics" },
]
this.form.valueChanges.subscribe(v => {
this.form.valueChanges.subscribe((v) => {
this.calculate()
})
}
form = this.fb.group({
things: this.fb.array([]),
tips: this.fb.control(''),
tips: this.fb.control("")
})
get things() {
@@ -86,25 +67,21 @@ export class GradesComponent implements OnInit, OnDestroy {
}
summary() {
this.router.navigate(['summary'], { relativeTo: this.route })
this.router.navigate(["summary"], { relativeTo: this.route })
}
attendenceSummary() {
this.router.navigate(['attendenceSummary'], { relativeTo: this.route })
this.router.navigate(["attendenceSummary"], {relativeTo: this.route})
}
ngOnInit(): void {
this.ac.getConfig().subscribe(s => {
this.ac.clean.getConfig().subscribe((s) => {
this.rooms = s.rooms
s.things.forEach(s =>
this.things.push(
this.fb.group({
cb: this.fb.control(false),
label: this.fb.control(s),
weight: this.fb.control(1),
})
)
)
s.things.forEach((s) => this.things.push(this.fb.group({
cb: this.fb.control(false),
label: this.fb.control(s),
weight: this.fb.control(1)
})))
})
}
@@ -114,19 +91,19 @@ export class GradesComponent implements OnInit, OnDestroy {
}
downloadData() {
this.ac.getClean(this.date(), this.room).subscribe(v => {
this.ac.clean.getClean(this.date, this.room).subscribe((v) => {
if (v) {
this.notes = v.notes
this.gradeDate = DateTime.fromISO(v.gradeDate)
this.gradeDate = moment(v.gradeDate)
this.grade = v.grade
this.id = v._id
this.form.get('tips')?.setValue(v.tips)
this.form.get("tips")?.setValue(v.tips)
} else {
this.gradeDate = undefined
this.grade = 6
this.notes = []
this.id = undefined
this.form.get('tips')?.setValue('')
this.form.get("tips")?.setValue("")
}
})
}
@@ -155,26 +132,26 @@ export class GradesComponent implements OnInit, OnDestroy {
weight.setValue(weight.value - 1)
}
}
},
}
}
save() {
this.calculate()
var obj = {
grade: this.grade,
date: this.date,
date: this.date.toDate(),
room: this.room,
notes: this.notes,
tips: this.form.get('tips')?.value,
tips: this.form.get("tips")?.value
}
this.ac.postClean(obj).subscribe(s => {
this.sb.open('Zapisano!', undefined, { duration: 1500 })
this.ac.clean.postClean(obj).subscribe((s) => {
this.sb.open("Zapisano!", undefined, { duration: 1500 })
this.downloadData()
})
}
remove() {
this.ac.delete(this.id!).subscribe(s => {
this.ac.clean.delete(this.id!).subscribe((s) => {
if (s.status == 200) {
this.downloadData()
}
@@ -187,35 +164,22 @@ export class GradesComponent implements OnInit, OnDestroy {
}
attendence() {
this.dialog
.open(AttendenceComponent, { data: { room: this.room } })
.afterClosed()
.subscribe(
(v: {
room: string
users: { att: boolean; id: string; hour: string }[]
notes: string
}) => {
if (!v) return
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.attendence
.postAttendence(x.room, { auto: x.users, notes: v.notes })
.subscribe(s => {
if (s.status == 200) {
this.sb.open('Zapisano obecność!', undefined, {
duration: 1500,
})
}
})
this.dialog.open(AttendenceComponent, {data: {room: this.room}}).afterClosed().subscribe((v: {room: string, users: {att: boolean, id: string, hour: string}[], notes: string}) => {
if (!v) return
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, {auto: x.users, notes: v.notes}).subscribe((s) => {
if (s.status == 200) {
this.sb.open("Zapisano obecność!", undefined, {duration: 1500})
}
})
})
}
}

View File

@@ -1,23 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { GradesService } from './grades.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('GradesService', () => {
let service: GradesService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(GradesService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,93 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DateTime } from 'luxon';
import { Status } from 'src/app/types/status';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class GradesService {
constructor(private http: HttpClient) { }
getConfig() {
return this.http.get<{ rooms: string[]; things: string[] }>(
environment.apiEndpoint + `/admin/clean/config`,
{ withCredentials: true }
)
}
getClean(date: DateTime, room: string) {
return this.http.get<{
_id: string
date: string
grade: number
gradeDate: string
notes: { label: string; weight: number }[]
room: string
tips: string
} | null>(environment.apiEndpoint + `/admin/clean/${date.toISODate()}/${room}`, {
withCredentials: true,
})
}
postClean(obj: Object) {
return this.http.post<Status>(
environment.apiEndpoint + `/admin/clean/`,
obj,
{ withCredentials: true }
)
}
delete(id: string) {
return this.http.delete<Status>(
environment.apiEndpoint + `/admin/clean/${id}`,
{ withCredentials: true }
)
}
summary = {
getSummary: (start: DateTime, end: DateTime) => {
return this.http.get<{ room: string; avg: number }[]>(
environment.apiEndpoint +
`/admin/clean/summary/${start.toISO()}/${end.toISO()}`,
{ withCredentials: true }
)
},
}
attendence = {
getUsers: (room: string) => {
return this.http.get<{
users: { fname: string; surname: string; _id: string }[]
attendence?: { auto: { id: string; hour?: string }[]; notes: string }
}>(environment.apiEndpoint + `/admin/clean/attendence/${room}`, {
withCredentials: true,
})
},
postAttendence: (
room: string,
attendence: { auto: { id: string; hour?: string }[]; notes: string }
) => {
return this.http.post<Status>(
environment.apiEndpoint + `/admin/clean/attendence/${room}`,
attendence,
{ withCredentials: true }
)
},
getSummary: () => {
return this.http.get<
{ room: string; hours: string[]; notes: string; auto: boolean }[]
>(environment.apiEndpoint + `/admin/clean/attendenceSummary`, {
withCredentials: true,
})
},
deleteRoom: (room: string) => {
return this.http.delete<Status>(
environment.apiEndpoint + `/admin/clean/attendence/${room}`,
{ withCredentials: true }
)
},
}
}

View File

@@ -1,33 +1,38 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SummaryComponent } from './summary.component'
import { RouterModule } from '@angular/router'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatDatepickerModule } from '@angular/material/datepicker'
import { MatIconModule } from '@angular/material/icon'
import { of } from 'rxjs'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MatTableModule } from '@angular/material/table'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter'
import { SummaryComponent } from './summary.component';
import { RouterModule } from '@angular/router';
import { AdminCommService } from '../../admin-comm.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MAT_MOMENT_DATE_FORMATS, MomentDateAdapter, provideMomentDateAdapter } from '@angular/material-moment-adapter';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
xdescribe('SummaryComponent', () => {
let component: SummaryComponent
let fixture: ComponentFixture<SummaryComponent>
describe('SummaryComponent', () => {
let component: SummaryComponent;
let fixture: ComponentFixture<SummaryComponent>;
beforeEach(async () => {
const acMock = {
clean: {
summary: {
getSummary: jasmine.createSpy('getSummary').and.returnValue(of()),
},
},
getSummary: jasmine.createSpy("getSummary").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({
declarations: [SummaryComponent],
providers: [
// { provide: AdminCommService, useValue: acMock },
provideLuxonDateAdapter(),
{ provide: AdminCommService, useValue: acMock },
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE, MAT_DATE_FORMATS, MAT_MOMENT_DATE_ADAPTER_OPTIONS] },
{ provide: MAT_DATE_LOCALE, useValue: "pl-PL" },
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } },
],
imports: [
RouterModule.forRoot([]),
@@ -37,16 +42,17 @@ xdescribe('SummaryComponent', () => {
FormsModule,
ReactiveFormsModule,
MatTableModule,
NoopAnimationsModule,
],
}).compileComponents()
NoopAnimationsModule
]
})
.compileComponents();
fixture = TestBed.createComponent(SummaryComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
fixture = TestBed.createComponent(SummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,68 +1,57 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ToolbarService } from '../../toolbar/toolbar.service'
import { ActivatedRoute, Router } from '@angular/router'
import { MatTableDataSource } from '@angular/material/table'
import { FormBuilder } from '@angular/forms'
import { MatSort } from '@angular/material/sort'
import { DateTime } from 'luxon'
import { GradesService } from '../grades.service'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ToolbarService } from '../../toolbar/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',
templateUrl: './summary.component.html',
styleUrl: './summary.component.scss',
standalone: false,
styleUrl: './summary.component.scss'
})
export class SummaryComponent implements OnInit, OnDestroy {
data: MatTableDataSource<{ room: string; avg: number }> =
new MatTableDataSource<{ room: string; avg: number }>()
data: MatTableDataSource<{room: string, avg: number}> = new MatTableDataSource<{room: string, avg: number}>();
collumns = ['room', 'avg']
dateSelector = this.fb.group({
start: this.fb.control(DateTime.utc().startOf('day')),
end: this.fb.control(DateTime.utc().endOf('day')),
start: this.fb.control(moment.utc().startOf('day')),
end: this.fb.control(moment.utc().endOf('day'))
})
@ViewChild(MatSort, { static: false }) set content(sort: MatSort) {
@ViewChild(MatSort, {static: false}) set content(sort: MatSort) {
this.data.sort = sort
}
constructor(
private toolbar: ToolbarService,
private router: Router,
private route: ActivatedRoute,
private ac: GradesService,
private fb: FormBuilder
) {
constructor (private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private ac: AdminCommService, private fb: FormBuilder) {
this.toolbar.comp = this
this.toolbar.menu = [
{ check: true, title: 'Ocenianie', fn: 'goBack', icon: 'arrow_back' },
{check: true, title: "Ocenianie", fn: "goBack", icon: "arrow_back"}
]
this.dateSelector.valueChanges.subscribe(v => {
this.dateSelector.valueChanges.subscribe((v) => {
this.download()
})
}
ngOnInit(): void {
this.download()
}
download() {
this.ac.summary
.getSummary(
this.dateSelector.get('start')?.value!.startOf('day')!,
this.dateSelector.get('end')?.value!.endOf('day')!
)
.subscribe(v => {
this.data.data = v
})
this.ac.clean.summary.getSummary(this.dateSelector.get('start')?.value!.startOf('day')!, this.dateSelector.get('end')?.value!.endOf('day')!).subscribe((v) => {
this.data.data = v
})
}
goBack() {
this.router.navigate(['../'], { relativeTo: this.route })
this.router.navigate(['../'], {relativeTo: this.route})
}
ngOnDestroy(): void {
this.toolbar.comp = undefined
this.toolbar.menu = undefined
}
}

View File

@@ -1,11 +1,9 @@
<button mat-raised-button color="accent" (click)="newGroup()">Nowa grupa</button>
@for (item of groups; track item) {
<mat-card>
<mat-card *ngFor="let item of groups">
<mat-card-header>
<mat-card-title contenteditable appCe (edit)="nameEdit(item._id, $event)">{{item.name}}</mat-card-title>
<mat-card-title contenteditable appCe (edit)="nameEdit(item._id, $event)">{{item.name}}</mat-card-title>
</mat-card-header>
<mat-card-actions>
<button mat-button color="warn" (click)="remove(item._id)">Usuń</button>
<button mat-button color="warn" (click)="remove(item._id)">Usuń</button>
</mat-card-actions>
</mat-card>
}
</mat-card>

View File

@@ -1,12 +1,12 @@
:host {
display: flex;
display: flex;
}
mat-card {
margin: 15px;
padding: 1ch;
margin: 15px;
padding: 1ch;
}
mat-card-title {
font-size: 1.5rem;
}
font-size: 1.5rem;
}

View File

@@ -1,28 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupsComponent } from './groups.component'
import { of } from 'rxjs'
import { GroupsComponent } from './groups.component';
import { AdminCommService } from '../admin-comm.service';
import { of } from 'rxjs';
xdescribe('GroupsComponent', () => {
let component: GroupsComponent
let fixture: ComponentFixture<GroupsComponent>
describe('GroupsComponent', () => {
let component: GroupsComponent;
let fixture: ComponentFixture<GroupsComponent>;
beforeEach(() => {
const acMock = {
groups: {
getGroups: jasmine.createSpy('getGroups').and.returnValue(of()),
},
getGroups: jasmine.createSpy("getGroups").and.returnValue(of())
}
}
TestBed.configureTestingModule({
declarations: [GroupsComponent],
// providers: [{ provide: AdminCommService, useValue: acMock }],
})
fixture = TestBed.createComponent(GroupsComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
providers: [
{provide: AdminCommService, useValue: acMock}
]
});
fixture = TestBed.createComponent(GroupsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,24 +1,20 @@
import { Component, OnInit } from '@angular/core'
import { Group } from 'src/app/types/group'
import { Status } from 'src/app/types/status'
import { MatDialog } from '@angular/material/dialog'
import { RemoveConfirmComponent } from './remove-confirm/remove-confirm.component'
import { GroupsService } from './groups.service'
import { Component, OnInit } from '@angular/core';
import { AdminCommService } from '../admin-comm.service';
import { Group } from 'src/app/types/group';
import { Status } from 'src/app/types/status';
import { MatDialog } from '@angular/material/dialog';
import { RemoveConfirmComponent } from './remove-confirm/remove-confirm.component';
@Component({
selector: 'app-groups',
templateUrl: './groups.component.html',
styleUrls: ['./groups.component.scss'],
standalone: false,
styleUrls: ['./groups.component.scss']
})
export class GroupsComponent implements OnInit {
groups?: Group[]
constructor(
protected readonly acs: GroupsService,
private readonly dialog: MatDialog
) {}
constructor (protected readonly acs: AdminCommService, private readonly dialog: MatDialog) {}
ngOnInit(): void {
this.acs.getGroups().subscribe(v => {
this.acs.groups.getGroups().subscribe((v) => {
this.groups = v
})
}
@@ -29,41 +25,36 @@ export class GroupsComponent implements OnInit {
}
}
get groupOptions(): { id: string; text: string }[] {
return this.groups!.map(v => {
return { id: v._id as string, text: v.name as string }
})
get groupOptions(): {id: string, text: string}[] {
return this.groups!.map((v)=> {return {id: v._id as string, text: v.name as string}})
}
protected getId(g: Group[] | undefined) {
if (!g) return undefined
return g.map(v => v._id)
return g.map((v)=>v._id)
}
groupNames(groups: Group[]) {
return groups.flatMap(g => g.name)
return groups.flatMap((g) => g.name)
}
protected nameEdit(id: string, name: string | string[]) {
name = name as string
this.acs.editName(id, name).subscribe(s => this.refreshIfGood(s))
this.acs.groups.editName(id, name).subscribe((s) => this.refreshIfGood(s))
}
protected newGroup() {
let name = prompt('Nazwa grupy')
let name = prompt("Nazwa grupy")
if (name) {
this.acs.newGroup(name).subscribe(s => this.refreshIfGood(s))
this.acs.groups.newGroup(name).subscribe((s) => this.refreshIfGood(s))
}
}
protected remove(id: string) {
this.dialog
.open(RemoveConfirmComponent)
.afterClosed()
.subscribe(v => {
if (v) {
this.acs.remove(id).subscribe(s => this.refreshIfGood(s))
}
})
this.dialog.open(RemoveConfirmComponent).afterClosed().subscribe((v) => {
if (v) {
this.acs.groups.remove(id).subscribe((s) => this.refreshIfGood(s))
}
})
}
}

View File

@@ -1,23 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { GroupsService } from './groups.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('GroupsService', () => {
let service: GroupsService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(GroupsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,46 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Group } from 'src/app/types/group';
import { Status } from 'src/app/types/status';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class GroupsService {
constructor(private http: HttpClient) { }
getGroups() {
return this.http.get<Group[]>(environment.apiEndpoint + `/admin/groups`, {
withCredentials: true,
})
}
newGroup(name: string) {
return this.http.post<Status>(
environment.apiEndpoint + `/admin/groups`,
{ name: name },
{ withCredentials: true }
)
}
editName(id: string, name: string) {
return this.putGroups(id, { name: name.trim() })
}
remove(id: string) {
return this.http.delete<Status>(
environment.apiEndpoint + `/admin/groups/${id}`,
{ withCredentials: true }
)
}
private putGroups(id: string, update: Partial<Group>) {
return this.http.put<Status>(
environment.apiEndpoint + `/admin/groups/${id}`,
update,
{ withCredentials: true }
)
}
}

View File

@@ -1,23 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemoveConfirmComponent } from './remove-confirm.component'
import { MatDialogModule } from '@angular/material/dialog'
import { RemoveConfirmComponent } from './remove-confirm.component';
import { MatDialogModule } from '@angular/material/dialog';
describe('RemoveConfirmComponent', () => {
let component: RemoveConfirmComponent
let fixture: ComponentFixture<RemoveConfirmComponent>
let component: RemoveConfirmComponent;
let fixture: ComponentFixture<RemoveConfirmComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [RemoveConfirmComponent],
imports: [MatDialogModule],
})
fixture = TestBed.createComponent(RemoveConfirmComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [MatDialogModule]
});
fixture = TestBed.createComponent(RemoveConfirmComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core'
import { Component } from '@angular/core';
@Component({
selector: 'app-remove-confirm',
templateUrl: './remove-confirm.component.html',
styleUrls: ['./remove-confirm.component.scss'],
standalone: false,
styleUrls: ['./remove-confirm.component.scss']
})
export class RemoveConfirmComponent {}
export class RemoveConfirmComponent {
}

View File

@@ -1,46 +1,42 @@
<div id="upper-bar">
<mat-form-field>
<mat-label>Wyszukaj</mat-label>
<input matInput (keyup)="filter($event)">
</mat-form-field>
<mat-chip-listbox [(ngModel)]="filters" multiple>
<mat-chip-option value="showAll">Pokaż wszystko</mat-chip-option>
</mat-chip-listbox>
<button mat-icon-button (click)="new()"><mat-icon>add</mat-icon></button>
<mat-form-field>
<mat-label>Wyszukaj</mat-label>
<input matInput (keyup)="filter($event)">
</mat-form-field>
<mat-chip-listbox [(ngModel)]="filters" multiple>
<mat-chip-option value="showAll">Pokaż wszystko</mat-chip-option>
</mat-chip-listbox>
<button mat-icon-button (click)="new()"><mat-icon>add</mat-icon></button>
</div>
@if (loading) {
<mat-spinner color="accent"></mat-spinner>
}
<mat-spinner *ngIf="loading" color="accent"></mat-spinner>
<table mat-table [dataSource]="keys">
<div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>Sala</th>
<td mat-cell *matCellDef="let element">{{element.room}}</td>
</div>
<div matColumnDef="whom">
<th mat-header-cell *matHeaderCellDef>Wypożyczający</th>
<td mat-cell *matCellDef="let element">{{element.whom.uname}}</td>
</div>
<div matColumnDef="borrow">
<th mat-header-cell *matHeaderCellDef>Data wypożyczenia</th>
<td mat-cell *matCellDef="let element">{{element.borrow.format("HH:mm, ddd D.MM.")}}</td>
</div>
<div matColumnDef="tb">
<th mat-header-cell *matHeaderCellDef>Data zwrotu</th>
<td mat-cell *matCellDef="let element">
@if (element.tb) {
{{element.tb.format("HH:mm, ddd D.MM.")}}
}
</td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Akcje</th>
<td mat-cell *matCellDef="let element">
@if (!element.tb) {
<button mat-mini-fab (click)="tb(element._id)"><mat-icon>person_cancel</mat-icon></button>
}
</td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let row; columns: collumns"></tr>
<div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>Sala</th>
<td mat-cell *matCellDef="let element">{{element.room}}</td>
</div>
<div matColumnDef="whom">
<th mat-header-cell *matHeaderCellDef>Wypożyczający</th>
<td mat-cell *matCellDef="let element">{{element.whom.uname}}</td>
</div>
<div matColumnDef="borrow">
<th mat-header-cell *matHeaderCellDef>Data wypożyczenia</th>
<td mat-cell *matCellDef="let element">{{element.borrow.format("HH:mm, ddd D.MM.")}}</td>
</div>
<div matColumnDef="tb">
<th mat-header-cell *matHeaderCellDef>Data zwrotu</th>
<td mat-cell *matCellDef="let element">
@if (element.tb) {
{{element.tb.format("HH:mm, ddd D.MM.")}}
}
</td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Akcje</th>
<td mat-cell *matCellDef="let element">
<button mat-mini-fab (click)="tb(element._id)" *ngIf="!element.tb"><mat-icon>person_cancel</mat-icon></button>
</td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let row; columns: collumns"></tr>
</table>
<mat-paginator pageSize="9" [pageSizeOptions]="[9, 15, 20, 50, 160]"></mat-paginator>

View File

@@ -1,5 +1,5 @@
#upper-bar {
display: flex;
align-items: baseline;
gap: 4pt;
}
display: flex;
align-items: baseline;
gap: 4pt;
}

View File

@@ -1,50 +1,44 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminKeyComponent } from './key.component'
import { of } from 'rxjs'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatChipsModule } from '@angular/material/chips'
import { MatIconModule } from '@angular/material/icon'
import { MatPaginatorModule } from '@angular/material/paginator'
import { FormsModule } from '@angular/forms'
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'
import { MatTableModule } from '@angular/material/table'
import { MatInputModule } from '@angular/material/input'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { AdminKeyComponent } from './key.component';
import { of } from 'rxjs';
import { AdminCommService } from '../admin-comm.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { FormsModule } from '@angular/forms';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
xdescribe('AdminKeyComponent', () => {
let component: AdminKeyComponent
let fixture: ComponentFixture<AdminKeyComponent>
describe('AdminKeyComponent', () => {
let component: AdminKeyComponent;
let fixture: ComponentFixture<AdminKeyComponent>;
let acMock
beforeEach(async () => {
acMock = {
keys: {
getKeys: jasmine.createSpy('getKeys').and.returnValue(of()),
},
getKeys: jasmine.createSpy("getKeys").and.returnValue(of())
}
}
await TestBed.configureTestingModule({
declarations: [AdminKeyComponent],
// providers: [{ provide: AdminCommService, useValue: acMock }],
imports: [
MatFormFieldModule,
MatChipsModule,
MatIconModule,
MatPaginatorModule,
FormsModule,
MatProgressSpinnerModule,
MatTableModule,
MatInputModule,
NoopAnimationsModule,
providers: [
{provide: AdminCommService, useValue: acMock}
],
}).compileComponents()
fixture = TestBed.createComponent(AdminKeyComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
imports: [MatFormFieldModule, MatChipsModule, MatIconModule, MatPaginatorModule, FormsModule, MatProgressSpinnerModule, MatTableModule, MatInputModule, NoopAnimationsModule]
})
.compileComponents();
fixture = TestBed.createComponent(AdminKeyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,50 +1,47 @@
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
import { MatPaginator } from '@angular/material/paginator'
import { MatTableDataSource } from '@angular/material/table'
import { AKey } from 'src/app/types/key'
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'
import { KeyService } from './key.service'
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import * as moment from 'moment';
import { AKey } from 'src/app/types/key';
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',
templateUrl: './key.component.html',
styleUrl: './key.component.scss',
standalone: false,
styleUrl: './key.component.scss'
})
export class AdminKeyComponent implements AfterViewInit, OnInit {
keys: MatTableDataSource<AKey> = new MatTableDataSource<AKey>()
keys: MatTableDataSource<AKey> = new MatTableDataSource<AKey>();
pureData: AKey[] = []
private _filters: string[] = []
private _filters: string[] = [];
public get filters(): string[] {
return this._filters
return this._filters;
}
collumns = ['room', 'whom', 'borrow', 'tb', 'actions']
public set filters(value: string[]) {
if (value.includes('showAll')) {
if (value.includes("showAll")) {
this.collumns = ['room', 'whom', 'borrow', 'tb', 'actions']
} else {
this.collumns = ['room', 'whom', 'borrow', 'actions']
}
this._filters = value
this.transformData()
this._filters = value;
this.transformData();
}
loading = true
@ViewChild(MatPaginator) paginator!: MatPaginator
constructor(
private ac: KeyService,
private dialog: MatDialog,
private sb: MatSnackBar
) {
constructor (private ac: AdminCommService, private dialog: MatDialog, private sb: MatSnackBar) {
this.filters = []
}
fetchData() {
this.loading = true
this.ac.getKeys().subscribe(r => {
this.ac.keys.getKeys().subscribe((r) => {
this.loading = false
this.pureData = r
this.transformData()
@@ -53,53 +50,45 @@ export class AdminKeyComponent implements AfterViewInit, OnInit {
transformData() {
var finalData: AKey[] = this.pureData
if (!this.filters.includes('showAll'))
finalData = finalData.filter(v => v.tb == undefined)
if (!this.filters.includes('showAll')) finalData = finalData.filter((v) => v.tb == undefined)
this.keys.data = finalData
}
filter(event: Event) {
const value = (event.target as HTMLInputElement).value
this.keys.filter = value.toLowerCase().trim()
}
ngAfterViewInit(): void {
this.keys.paginator = this.paginator
}
ngOnInit(): void {
this.fetchData()
// [
// {room: "Kawiarenka", borrow: moment().subtract(15, "minutes"), whom: {_id: "test", room: 303, uname: "sk"}}
// ]
}
new() {
this.dialog
.open(NewKeyComponent)
.afterClosed()
.subscribe(v => {
if (v) {
this.ac
.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()
}
})
}
})
this.dialog.open(NewKeyComponent).afterClosed().subscribe(v => {
if (v) {
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()
}
})
}
})
}
tb(id: string) {
this.ac.returnKey(id).subscribe(r => {
this.ac.keys.returnKey(id).subscribe((r) => {
if (r.status == 200) {
this.fetchData()
}

View File

@@ -1,23 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { KeyService } from './key.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('KeyService', () => {
let service: KeyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(KeyService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,62 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DateTime } from 'luxon';
import { map } from 'rxjs';
import { AKey } from 'src/app/types/key';
import { Status } from 'src/app/types/status';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class KeyService {
constructor(
private http: HttpClient
) { }
getKeys() {
return this.http
.get<
(Omit<AKey, 'borrow' | 'tb'> & { borrow: string; tb?: string })[]
>(environment.apiEndpoint + `/admin/keys`, { withCredentials: true })
.pipe(
map(v => {
return v.map(r => {
let newkey: any = { ...r }
newkey.borrow = DateTime.fromISO(r.borrow!)
if (newkey.tb) newkey.tb = DateTime.fromISO(r.tb!)
return newkey as AKey
})
})
)
}
avalKeys() {
return this.http.get<string[]>(
environment.apiEndpoint + `/admin/keys/available`,
{ withCredentials: true }
)
}
postKey(room: string, uname: string) {
return this.http.post<Status>(
environment.apiEndpoint + `/admin/keys/`,
{ room: room, whom: uname },
{ withCredentials: true }
)
}
returnKey(id: string) {
return this.putKeys(id, { tb: DateTime.now() })
}
private putKeys(id: string, update: Partial<AKey>) {
return this.http.put<Status>(
environment.apiEndpoint + `/admin/keys/${id}`,
update,
{ withCredentials: true }
)
}
}

View File

@@ -1,23 +1,19 @@
<mat-dialog-content>
<form (ngSubmit)="send()" [formGroup]="form">
<mat-form-field color="accent">
<mat-label>Sala</mat-label>
<mat-select formControlName="room" required>
@for (item of rooms; track $index) {
<mat-option [value]="item">{{item}}</mat-option>
}
</mat-select>
@if (form.controls['room'].hasError('required')) {
<mat-error>Wymagane</mat-error>
}
</mat-form-field>
<mat-form-field color="accent">
<mat-label>Wypożyczający</mat-label>
<app-user-search formControlName="user" required/>
@if (form.controls['user'].hasError('required')) {
<mat-error>Wymagane</mat-error>
}
</mat-form-field>
<button mat-button>Wyślij</button>
</form>
<form (ngSubmit)="send()" [formGroup]="form">
<mat-form-field color="accent">
<mat-label>Sala</mat-label>
<mat-select formControlName="room" required>
@for (item of rooms; track $index) {
<mat-option [value]="item">{{item}}</mat-option>
}
</mat-select>
<mat-error *ngIf="form.controls['room'].hasError('required')">Wymagane</mat-error>
</mat-form-field>
<mat-form-field color="accent">
<mat-label>Wypożyczający</mat-label>
<app-user-search formControlName="user" required/>
<mat-error *ngIf="form.controls['user'].hasError('required')">Wymagane</mat-error>
</mat-form-field>
<button mat-button>Wyślij</button>
</form>
</mat-dialog-content>

View File

@@ -1,4 +1,4 @@
form {
display: flex;
flex-direction: column;
}
display: flex;
flex-direction: column;
}

View File

@@ -1,57 +1,41 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewKeyComponent } from './new-key.component'
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'
import {
MatFormFieldControl,
MatFormFieldModule,
} from '@angular/material/form-field'
import { MatSelectModule } from '@angular/material/select'
import { Component, forwardRef, Optional, Self } from '@angular/core'
import { Observable, of } from 'rxjs'
import {
AbstractControlDirective,
ControlValueAccessor,
FormsModule,
NG_VALUE_ACCESSOR,
NgControl,
ReactiveFormsModule,
} from '@angular/forms'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { NewKeyComponent } from './new-key.component';
import { AdminCommService } from '../../admin-comm.service';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { Component, forwardRef, Optional, Self } from '@angular/core';
import { Observable, of } from 'rxjs';
import { AbstractControlDirective, ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: 'app-user-search',
template: '',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserSearchStub),
multi: true,
},
{
provide: MatFormFieldControl,
useExisting: UserSearchStub,
},
],
standalone: false,
selector: "app-user-search", template: '', providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserSearchStub),
multi: true,
},
{
provide: MatFormFieldControl,
useExisting: UserSearchStub
}]
})
class UserSearchStub
implements ControlValueAccessor, MatFormFieldControl<never>
{
value: null = null
stateChanges: Observable<void> = of()
id: string = ''
placeholder: string = ''
ngControl: NgControl | AbstractControlDirective | null = null
focused: boolean = false
empty: boolean = true
shouldLabelFloat: boolean = true
required: boolean = false
disabled: boolean = false
errorState: boolean = false
controlType?: string | undefined
autofilled?: boolean | undefined
userAriaDescribedBy?: string | undefined
class UserSearchStub implements ControlValueAccessor, MatFormFieldControl<never> {
value: null = null;
stateChanges: Observable<void> = of();
id: string = "";
placeholder: string = "";
ngControl: NgControl | AbstractControlDirective | null = null;
focused: boolean = false;
empty: boolean = true;
shouldLabelFloat: boolean = true;
required: boolean = false;
disabled: boolean = false;
errorState: boolean = false;
controlType?: string | undefined;
autofilled?: boolean | undefined;
userAriaDescribedBy?: string | undefined;
setDescribedByIds(ids: string[]): void {}
onContainerClick(event: MouseEvent): void {}
writeValue(obj: any): void {}
@@ -60,22 +44,22 @@ class UserSearchStub
setDisabledState?(isDisabled: boolean): void {}
}
xdescribe('NewKeyComponent', () => {
let component: NewKeyComponent
let fixture: ComponentFixture<NewKeyComponent>
describe('NewKeyComponent', () => {
let component: NewKeyComponent;
let fixture: ComponentFixture<NewKeyComponent>;
let acMock
beforeEach(async () => {
acMock = {
keys: {
avalKeys: jasmine.createSpy('avalKeys').and.returnValue(of()),
},
avalKeys: jasmine.createSpy("avalKeys").and.returnValue(of())
}
}
await TestBed.configureTestingModule({
declarations: [NewKeyComponent, UserSearchStub],
providers: [
// { provide: AdminCommService, useValue: acMock },
{ provide: MatDialogRef, useValue: {} },
{ provide: AdminCommService, useValue: acMock },
{ provide: MatDialogRef, useValue: {} }
],
imports: [
MatDialogModule,
@@ -83,16 +67,17 @@ xdescribe('NewKeyComponent', () => {
MatSelectModule,
FormsModule,
ReactiveFormsModule,
NoopAnimationsModule,
],
}).compileComponents()
NoopAnimationsModule
]
})
.compileComponents();
fixture = TestBed.createComponent(NewKeyComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
fixture = TestBed.createComponent(NewKeyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,29 +1,25 @@
import { Component, OnInit } from '@angular/core'
import { MatDialogRef } from '@angular/material/dialog'
import { FormControl, FormGroup } from '@angular/forms'
import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component'
import { KeyService } from '../key.service'
import { Component, OnInit } from '@angular/core';
import { AdminCommService } from '../../admin-comm.service';
import { MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormGroup } from '@angular/forms';
import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component';
@Component({
selector: 'app-new-key',
templateUrl: './new-key.component.html',
styleUrl: './new-key.component.scss',
standalone: false,
styleUrl: './new-key.component.scss'
})
export class NewKeyComponent implements OnInit {
rooms: string[] = []
form = new FormGroup({
room: new FormControl<string>(''),
user: new FormControl<UserSearchResult | null>(null),
room: new FormControl<string>(""),
user: new FormControl<UserSearchResult | null>(null)
})
unames: any[] = []
constructor(
private ac: KeyService,
public dialogRef: MatDialogRef<NewKeyComponent>
) {}
constructor ( private ac: AdminCommService, public dialogRef: MatDialogRef<NewKeyComponent> ) {}
ngOnInit(): void {
this.ac.avalKeys().subscribe(v => {
this.ac.keys.avalKeys().subscribe((v) => {
this.rooms = v
})
}
@@ -31,6 +27,7 @@ export class NewKeyComponent implements OnInit {
send() {
if (this.form.valid) {
this.dialogRef.close(this.form.value)
}
}
}
}

View File

@@ -1,39 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { MenuAddComponent } from './menu-add.component'
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog'
import { MatRadioModule } from '@angular/material/radio'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
describe('MenuAddComponent', () => {
let component: MenuAddComponent
let fixture: ComponentFixture<MenuAddComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MenuAddComponent],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} },
],
imports: [
MatDialogModule,
MatRadioModule,
ReactiveFormsModule,
FormsModule,
],
}).compileComponents()
fixture = TestBed.createComponent(MenuAddComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View File

@@ -1,61 +0,0 @@
import { Component, signal } from '@angular/core'
import { MenuUploadComponent } from '../menu-upload/menu-upload.component'
import { MatDialog, MatDialogRef } from '@angular/material/dialog'
import { FDSelection } from 'src/app/fd.da'
import { FormControl, FormGroup } from '@angular/forms'
import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker'
import { DateTime } from 'luxon'
import { weekendFilter } from 'src/app/util'
@Component({
selector: 'app-menu-add',
templateUrl: './menu-add.component.html',
styleUrl: './menu-add.component.scss',
providers: [
{ provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection },
],
standalone: false,
})
export class MenuAddComponent {
type: string | undefined
filter = weekendFilter
day = signal<DateTime>(DateTime.now())
range = new FormGroup({
start: new FormControl<DateTime | null>(null),
end: new FormControl<DateTime | null>(null),
})
constructor(
public dialogRef: MatDialogRef<MenuAddComponent>,
private dialog: MatDialog
) {}
submit() {
switch (this.type) {
case 'day':
this.dialogRef.close({ type: 'day', value: this.day().toISODate() })
break
case 'week':
this.dialogRef.close({
type: 'week',
value: { start: this.range.value.start?.toISODate(), count: 5 },
})
break
default:
break
}
}
activateUpload() {
this.dialog
.open(MenuUploadComponent)
.afterClosed()
.subscribe(data => {
if (data) {
this.dialogRef.close({ type: 'file', ...data })
}
})
}
}

View File

@@ -1,155 +0,0 @@
<div id="upper-bar">
<mat-form-field subscriptSizing="dynamic">
<mat-label>Wybierz tydzień</mat-label>
<mat-date-range-input [rangePicker]="picker" [formGroup]="range">
<input matStartDate formControlName="start">
<input matEndDate formControlName="end">
</mat-date-range-input>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<button mat-icon-button (click)="ac.refresh()"><mat-icon>search</mat-icon></button>
<button mat-icon-button (click)="addDate()"><mat-icon>add</mat-icon></button>
<button mat-icon-button (click)="print()"><mat-icon>print</mat-icon></button>
</div>
@if (loading) {
<mat-spinner></mat-spinner>
}
<div class="mainc">
@switch (ac.state()) {
@case (0) {
<p>Wybierz zakres dat powyżej i kliknij szukaj</p>
}
@case (1) {
<div class="spinner">
<mat-spinner color="accent"/>
</div>
}
@case (2) {
<table mat-table [dataSource]="dataSource">
<div matColumnDef="day">
<th mat-header-cell *matHeaderCellDef>Dzień</th>
<td mat-cell *matCellDef="let element">
<span>{{element.day.toFormat('dd.LL.yyyy')}}r.</span>
<p>{{element.day.toFormat('cccc') | titlecase }}</p>
<app-field-editor category="Nazwa" [(word)]="element.dayTitle" (wordChange)="editTitle(element._id)"/><br><hr>
<button (click)="remove(element._id)">Usuń dzień</button>
</td>
</div>
<div matColumnDef="sn">
<th mat-header-cell *matHeaderCellDef>Śniadanie</th>
<td mat-cell *matCellDef="let element">
<ul class="non-editable">
@for (i of ls.defaultItems.sn; track i) {
<li>{{i}}</li>
}
</ul><hr>
<app-list-editor [(list)]="element.sn.fancy" (edit)="editSn(element._id)" dataList="sn-fancy"/><hr>
<ul>
<li><app-field-editor category="II Śniadanie" [(word)]="element.sn.second" list="sn-second" (wordChange)="editSn(element._id)"/></li>
</ul>
</td>
</div>
<div matColumnDef="ob">
<th mat-header-cell *matHeaderCellDef>Obiad</th>
<td mat-cell *matCellDef="let element">
<ul>
<li><app-field-editor category="Zupa" [(word)]="element.ob.soup" list="ob-soup" (wordChange)="editOb(element._id)"/></li>
<li><app-field-editor category="Vege" [(word)]="element.ob.vege" list="ob-vege" (wordChange)="editOb(element._id)"/></li>
<li><app-field-editor category="Danie główne" [(word)]="element.ob.meal" list="ob-meal" (wordChange)="editOb(element._id)"/></li>
</ul><hr>
<app-list-editor [(list)]="element.ob.condiments" (edit)="editOb(element._id)" dataList="ob-condiments"/><hr>
<ul>
<li><app-field-editor category="Napój" [(word)]="element.ob.drink" list="ob-drink" (wordChange)="editOb(element._id)"/></li>
</ul><hr>
<app-list-editor [(list)]="element.ob.other" (edit)="editOb(element._id)" dataList="ob-other"/>
<button (click)="getStat(element.day, 'ob')">
Opinie wychowanków
</button>
</td>
</div>
<div matColumnDef="kol">
<th mat-header-cell *matHeaderCellDef>Kolacja</th>
<td mat-cell *matCellDef="let element">
<div>
@switch (element.day.weekday) {
@default {
<div>
<ul class="non-editable">
@for (i of ls.defaultItems.kol; track i) {
<li>{{i}}</li>
}
</ul><hr>
<ul>
<li><app-field-editor category="Kolacja" [(word)]="element.kol" list="kol" (wordChange)="editKol(element._id)"/></li>
</ul>
<button (click)="getStat(element.day, 'kol')">
Opinie wychowanków
</button>
</div>
}
@case (5) {
<div class="non-editable">
<p>Kolacja w domu!</p>
<p>(Nie edytowalne)</p>
</div>
}
}
</div>
</td>
</div>
<tr mat-header-row *matHeaderRowDef="dcols"></tr>
<tr mat-row *matRowDef="let row; columns: dcols"></tr>
</table>
}
}
</div>
@if (options) {
<datalist id="sn-fancy">
@for (i of options.sn.fancy; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="sn-second">
@for (i of options.sn.second; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="ob-soup">
@for (i of options.ob.soup; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="ob-vege">
@for (i of options.ob.vege; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="ob-meal">
@for (i of options.ob.meal; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="ob-condiments">
@for (i of options.ob.condiments; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="ob-drink">
@for (i of options.ob.drink; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="ob-other">
@for (i of options.ob.other; track i) {
<option>{{i}}</option>
}
</datalist>
<datalist id="kol">
@for (i of options.kol; track i) {
<option>{{i}}</option>
}
</datalist>
}

View File

@@ -1,34 +0,0 @@
#upper-bar {
display: flex;
}
mat-form-field {
flex-grow: 1;
}
button[mat-icon-button] {
margin-left: 4pt;
margin-right: 4pt;
margin-top: 4pt;
}
.non-editable {
color: gray;
font-style: italic;
}
.mainc {
height: 100%;
position: relative;
}
.spinner {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,51 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { MenuEditComponent } from './menu-edit.component'
import { MatTableModule } from '@angular/material/table'
import { MatInputModule } from '@angular/material/input'
import {
MAT_DATE_RANGE_SELECTION_STRATEGY,
MatDatepickerModule,
} from '@angular/material/datepicker'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { FDSelection } from 'src/app/fd.da'
import { ReactiveFormsModule } from '@angular/forms'
import { of } from 'rxjs'
import { MatDialogModule } from '@angular/material/dialog'
import { MatIconModule } from '@angular/material/icon'
import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter'
xdescribe('MenuEditComponent', () => {
let component: MenuEditComponent
let fixture: ComponentFixture<MenuEditComponent>
beforeEach(() => {
const acMock = jasmine.createSpyObj('AdminCommService', {
getMenu: of(),
})
TestBed.configureTestingModule({
declarations: [MenuEditComponent],
imports: [
MatTableModule,
MatInputModule,
MatDatepickerModule,
BrowserAnimationsModule,
ReactiveFormsModule,
MatDialogModule,
MatIconModule,
],
providers: [
provideLuxonDateAdapter(),
{ provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection },
// { provide: AdminCommService, useValue: acMock },
],
})
fixture = TestBed.createComponent(MenuEditComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View File

@@ -1,152 +0,0 @@
import { Component } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker'
import { FDSelection } from 'src/app/fd.da'
import { Menu } from 'src/app/types/menu'
import { MatTableDataSource } from '@angular/material/table'
import { MatDialog } from '@angular/material/dialog'
import { MenuUploadComponent } from './menu-upload/menu-upload.component'
import { Status } from 'src/app/types/status'
import { MatSnackBar } from '@angular/material/snack-bar'
import { MenuAddComponent } from './menu-add/menu-add.component'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { DateTime } from 'luxon'
import { MenuEditService } from './menu-edit.service'
@Component({
selector: 'app-menu-edit',
templateUrl: './menu-edit.component.html',
styleUrls: ['./menu-edit.component.scss'],
providers: [
{ provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection },
],
standalone: false,
})
export class MenuEditComponent {
dcols: string[] = ['day', 'sn', 'ob', 'kol']
dataSource: MatTableDataSource<Menu> = new MatTableDataSource<Menu>()
range = new FormGroup({
start: new FormControl<DateTime | null>(null),
end: new FormControl<DateTime | null>(null),
})
loading = false
public options: any
constructor(
protected ac: MenuEditService,
private dialog: MatDialog,
private sb: MatSnackBar,
readonly ls: LocalStorageService
) {
this.range.valueChanges.subscribe(v => {
ac.setDates(v.start!, v.end!)
})
ac.menuItems.subscribe(v => {
this.dataSource.data = v
})
}
print() {
this.ac
.print(this.range.value.start, this.range.value.end)
?.subscribe(r => {
if (r && r.length > 0) {
var mywindow = window.open(
undefined,
'Drukowanie',
'height=400,width=400'
)
mywindow?.document.write(r)
mywindow?.print()
mywindow?.close()
}
})
}
addDate() {
this.dialog
.open(MenuAddComponent)
.afterClosed()
.subscribe(data => {
if (data) {
switch (data.type) {
case 'day':
this.ac.new
.single(data.value)
.subscribe(s => this.refreshIfGood(s))
break
case 'week':
this.ac.new
.range(data.value.start, data.value.count)
.subscribe(s => this.refreshIfGood(s))
break
case 'file':
this.refresh()
break
default:
break
}
}
})
}
refresh() {
this.ac.refresh()
this.ac.getOpts().subscribe(o => {
this.options = o
})
}
private refreshIfGood(s: Status) {
if (s.status.toString().match(/2\d\d/)) this.refresh()
}
activateUpload() {
this.dialog
.open(MenuUploadComponent)
.afterClosed()
.subscribe(data => {
if (data) this.refresh()
})
}
editSn(id: string) {
this.ac
.editSn(id, this.dataSource.data.find(v => v._id == id)!.sn)
.subscribe(s => this.refreshIfGood(s))
}
editOb(id: string) {
this.ac
.editOb(id, this.dataSource.data.find(v => v._id == id)!.ob)
.subscribe(s => this.refreshIfGood(s))
}
editKol(id: string) {
this.ac
.editKol(id, this.dataSource.data.find(v => v._id == id)?.kol)
.subscribe(s => this.refreshIfGood(s))
}
editTitle(id: string) {
this.ac
.editTitle(id, this.dataSource.data.find(v => v._id == id)?.dayTitle)
.subscribe(s => this.refreshIfGood(s))
}
getStat(day: DateTime, m: 'ob' | 'kol') {
this.ac
.stat(day, m)
.subscribe(s =>
this.sb.open(
`${s.y} / ${s.y + s.n} = ${((s.y / (s.y + s.n)) * 100).toFixed(2)}%`,
'Zamknij',
{ duration: 2500 }
)
)
}
remove(id: string) {
this.ac.rm(id).subscribe(s => this.refreshIfGood(s))
}
}

View File

@@ -1,23 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { MenuEditService } from './menu-edit.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('MenuEditService', () => {
let service: MenuEditService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(MenuEditService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,150 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, signal } from '@angular/core';
import { DateTime } from 'luxon';
import { BehaviorSubject, catchError, map, of } from 'rxjs';
import { Menu, MenuAPI } from 'src/app/types/menu';
import { STATE } from 'src/app/types/state';
import { Status } from 'src/app/types/status';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class MenuEditService {
private _menuItems = new BehaviorSubject<Menu[]>([])
public readonly menuItems = this._menuItems.asObservable()
private _state = signal(STATE.NOT_LOADED);
public readonly state = this._state.asReadonly();
private _error = signal<string | undefined>(undefined);
public readonly error = this._error.asReadonly();
private seDates: {
start: DateTime | null,
end: DateTime | null
} = {
start: null,
end: null
}
public setDates(start: DateTime | null, end: DateTime | null) {
this.seDates.start = start
this.seDates.end = end
}
public refresh() {
this.getMenu()
}
private getMenu() {
if (!(this.seDates.start && this.seDates.end)) return
this._state.set(STATE.PENDING)
const body = { start: this.seDates.start.toString(), end: this.seDates.end.toString() }
this.http.get
<MenuAPI[]>
(environment.apiEndpoint + `/admin/menu`, { withCredentials: true, params: body })
.pipe(
catchError((err: Error) => {
this._state.set(STATE.ERROR)
this._error.set(err.message)
return of()
}),
map<MenuAPI[], Menu[]>(v =>
v.map(i => ({
...i,
day: DateTime.fromISO(i.day)
})))
).subscribe(v => {
this._error.set(undefined)
this._menuItems.next(v ?? [])
this._state.set(STATE.LOADED)
})
}
constructor(private http: HttpClient) { }
getOpts() {
return this.http.get<any>(environment.apiEndpoint + `/admin/menu/opts`, {
withCredentials: true,
})
}
postMenu(file: File) {
if (file) {
const formData = new FormData()
formData.append('menu', file)
return this.http.post<Status>(
environment.apiEndpoint + '/admin/menu/upload',
formData,
{ withCredentials: true }
)
}
return
}
editSn(id: string, content: Menu['sn']) {
return this.putMenu(id, { sn: content })
}
editOb(id: string, content: Menu['ob']) {
return this.putMenu(id, { ob: content })
}
editKol(id: string, content: Menu['kol']) {
return this.putMenu(id, { kol: content })
}
editTitle(id: string, content: Menu['dayTitle']) {
return this.putMenu(id, { dayTitle: content })
}
print(start?: DateTime | null, end?: DateTime | null) {
if (start && end) {
const body = { start: start.toString(), end: end.toString() }
return this.http.get(environment.apiEndpoint + '/admin/menu/print', {
withCredentials: true,
params: body,
responseType: 'text',
})
}
return
}
stat(day: DateTime, m: 'ob' | 'kol') {
return this.http.get<{ y: number; n: number }>(
environment.apiEndpoint + `/admin/menu/${day.toISO()}/votes/${m}`,
{ withCredentials: true }
)
}
new = {
single: (day: DateTime) => {
return this.http.post<Status>(
environment.apiEndpoint + `/admin/menu/${day.toISO()}`,
null,
{ withCredentials: true }
)
},
range: (start: DateTime, count: number) => {
return this.http.post<Status>(
environment.apiEndpoint + `/admin/menu/${start.toISO()}/${count}/`,
null,
{ withCredentials: true }
)
},
}
rm(id: string) {
return this.http.delete<Status>(
environment.apiEndpoint + `/admin/menu/${id}`,
{ withCredentials: true }
)
}
private putMenu(id: string, update: Partial<Menu>) {
return this.http.put<Status>(
environment.apiEndpoint + `/admin/menu/${id}`,
update,
{ withCredentials: true }
)
}
}

View File

@@ -1,4 +0,0 @@
:host {
margin: 8pt;
display: block;
}

View File

@@ -1,28 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { MenuUploadComponent } from './menu-upload.component'
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'
xdescribe('MenuUploadComponent', () => {
let component: MenuUploadComponent
let fixture: ComponentFixture<MenuUploadComponent>
beforeEach(() => {
const acMock = jasmine.createSpyObj('AdminCommService', ['postMenu'])
TestBed.configureTestingModule({
declarations: [MenuUploadComponent],
providers: [
// { provide: AdminCommService, useValue: acMock },
{ provide: MatDialogRef, useValue: {} },
],
imports: [MatDialogModule],
})
fixture = TestBed.createComponent(MenuUploadComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View File

@@ -1,31 +0,0 @@
import { Component } from '@angular/core'
import { MatDialogRef } from '@angular/material/dialog'
import { MenuEditService } from '../menu-edit.service'
@Component({
selector: 'app-upload-edit',
templateUrl: './menu-upload.component.html',
styleUrls: ['./menu-upload.component.scss'],
standalone: false,
})
export class MenuUploadComponent {
constructor(
private ac: MenuEditService,
public dialogRef: MatDialogRef<MenuUploadComponent>
) {}
protected file: File | undefined
onFileChange(event: Event) {
const file: File = (event.target as HTMLInputElement).files![0]
if (file) {
this.file = file
} else {
this.file = undefined
}
}
submit() {
this.ac.postMenu(this.file!)?.subscribe(value => {
this.dialogRef.close(value)
})
}
}

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuAddComponent } from './menu-add.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatRadioModule } from '@angular/material/radio';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
describe('MenuAddComponent', () => {
let component: MenuAddComponent;
let fixture: ComponentFixture<MenuAddComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MenuAddComponent],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {}},
{provide: MatDialogRef, useValue: {}}
],
imports: [
MatDialogModule,
MatRadioModule,
ReactiveFormsModule,
FormsModule
]
})
.compileComponents();
fixture = TestBed.createComponent(MenuAddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,51 @@
import { Component } from '@angular/core';
import { MenuUploadComponent } from '../menu-upload/menu-upload.component';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { FDSelection, weekendFilter } from 'src/app/fd.da';
import { FormControl, FormGroup } from '@angular/forms';
import { Moment } from 'moment';
import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker';
import * as moment from 'moment';
@Component({
selector: 'app-menu-add',
templateUrl: './menu-add.component.html',
styleUrl: './menu-add.component.scss',
providers: [
{provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection}
]
})
export class MenuAddComponent {
type: string | undefined;
filter = weekendFilter
day: Moment = moment.utc();
range = new FormGroup({
start: new FormControl<Moment|null>(null),
end: new FormControl<Moment|null>(null),
})
constructor (public dialogRef: MatDialogRef<MenuAddComponent>, private dialog: MatDialog) { }
submit() {
switch (this.type) {
case "day":
this.dialogRef.close({type: "day", value: this.day.utc().startOf('day')})
break;
case "week":
this.dialogRef.close({type: "week", value: {start: this.range.value.start?.utc().hours(24), count: 5}})
break;
default:
break;
}
}
activateUpload() {
this.dialog.open(MenuUploadComponent).afterClosed().subscribe((data) => {
if (data) {
this.dialogRef.close({type: "file", ...data});
}
})
}
}

View File

@@ -0,0 +1,111 @@
<div id="upper-bar">
<mat-form-field>
<mat-label>Wybierz tydzień</mat-label>
<mat-date-range-input [rangePicker]="picker" [formGroup]="range">
<input matStartDate formControlName="start" (dateChange)="requestData()">
<input matEndDate formControlName="end" (dateChange)="requestData()">
</mat-date-range-input>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<button mat-icon-button (click)="requestData()"><mat-icon>refresh</mat-icon></button>
<button mat-icon-button (click)="addDate()"><mat-icon>add</mat-icon></button>
<button mat-icon-button (click)="print()"><mat-icon>print</mat-icon></button>
</div>
<mat-spinner *ngIf="loading"></mat-spinner>
<table mat-table [dataSource]="dataSource">
<div matColumnDef="day">
<th mat-header-cell *matHeaderCellDef>Dzień</th>
<td mat-cell *matCellDef="let element">
<span>{{element.day.format('DD.MM.YYYY')}}r.</span>
<p>{{element.day.format('dddd')}}</p>
<app-field-editor category="Nazwa" [(word)]="element.dayTitle" (wordChange)="editTitle(element._id)"/><br><hr>
<button (click)="remove(element._id)">Usuń dzień</button>
</td>
</div>
<div matColumnDef="sn">
<th mat-header-cell *matHeaderCellDef>Śniadanie</th>
<td mat-cell *matCellDef="let element">
<ul class="non-editable">
<li *ngFor="let i of ls.defaultItems.sn">{{i}}</li>
</ul><hr>
<app-list-editor [(list)]="element.sn.fancy" (edit)="editSn(element._id)" dataList="sn-fancy"/><hr>
<ul>
<li><app-field-editor category="II Śniadanie" [(word)]="element.sn.second" list="sn-second" (wordChange)="editSn(element._id)"/></li>
</ul>
</td>
</div>
<div matColumnDef="ob">
<th mat-header-cell *matHeaderCellDef>Obiad</th>
<td mat-cell *matCellDef="let element">
<ul>
<li><app-field-editor category="Zupa" [(word)]="element.ob.soup" list="ob-soup" (wordChange)="editOb(element._id)"/></li>
<li><app-field-editor category="Vege" [(word)]="element.ob.vege" list="ob-vege" (wordChange)="editOb(element._id)"/></li>
<li><app-field-editor category="Danie główne" [(word)]="element.ob.meal" list="ob-meal" (wordChange)="editOb(element._id)"/></li>
</ul><hr>
<app-list-editor [(list)]="element.ob.condiments" (edit)="editOb(element._id)" dataList="ob-condiments"/><hr>
<ul>
<li><app-field-editor category="Napój" [(word)]="element.ob.drink" list="ob-drink" (wordChange)="editOb(element._id)"/></li>
</ul><hr>
<app-list-editor [(list)]="element.ob.other" (edit)="editOb(element._id)" dataList="ob-other"/>
<button (click)="getStat(element.day, 'ob')">
Opinie wychowanków
</button>
</td>
</div>
<div matColumnDef="kol">
<th mat-header-cell *matHeaderCellDef>Kolacja</th>
<td mat-cell *matCellDef="let element">
<div [ngSwitch]="element.day.isoWeekday()">
<div *ngSwitchDefault>
<ul class="non-editable">
<li *ngFor="let i of ls.defaultItems.kol">{{i}}</li>
</ul><hr>
<ul>
<li><app-field-editor category="Kolacja" [(word)]="element.kol" list="kol" (wordChange)="editKol(element._id)"/></li>
</ul>
<button (click)="getStat(element.day, 'kol')">
Opinie wychowanków
</button>
</div>
<div *ngSwitchCase="5" class="non-editable">
<p>Kolacja w domu!</p>
<p>(Nie edytowalne)</p>
</div>
</div>
</td>
</div>
<tr mat-header-row *matHeaderRowDef="dcols"></tr>
<tr mat-row *matRowDef="let row; columns: dcols"></tr>
</table>
<ng-container *ngIf="options">
<datalist id="sn-fancy">
<option *ngFor="let i of options.sn.fancy">{{i}}</option>
</datalist>
<datalist id="sn-second">
<option *ngFor="let i of options.sn.second">{{i}}</option>
</datalist>
<datalist id="ob-soup">
<option *ngFor="let i of options.ob.soup">{{i}}</option>
</datalist>
<datalist id="ob-vege">
<option *ngFor="let i of options.ob.vege">{{i}}</option>
</datalist>
<datalist id="ob-meal">
<option *ngFor="let i of options.ob.meal">{{i}}</option>
</datalist>
<datalist id="ob-condiments">
<option *ngFor="let i of options.ob.condiments">{{i}}</option>
</datalist>
<datalist id="ob-drink">
<option *ngFor="let i of options.ob.drink">{{i}}</option>
</datalist>
<datalist id="ob-other">
<option *ngFor="let i of options.ob.other">{{i}}</option>
</datalist>
<datalist id="kol">
<option *ngFor="let i of options.kol">{{i}}</option>
</datalist>
</ng-container>

View File

@@ -0,0 +1,18 @@
#upper-bar {
display: flex;
}
mat-form-field {
flex-grow: 1;
}
button[mat-icon-button] {
margin-left: 4pt;
margin-right: 4pt;
margin-top: 4pt;
}
.non-editable {
color: gray;
font-style: italic;
}

View File

@@ -0,0 +1,45 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuNewComponent } from './menu-new.component';
import { MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input';
import { MAT_DATE_RANGE_SELECTION_STRATEGY, MatDatepickerModule } from '@angular/material/datepicker';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from '@angular/material-moment-adapter';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FDSelection } from 'src/app/fd.da';
import { ReactiveFormsModule } from '@angular/forms';
import { AdminCommService } from '../admin-comm.service';
import { of } from 'rxjs';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
describe('MenuNewComponent', () => {
let component: MenuNewComponent;
let fixture: ComponentFixture<MenuNewComponent>;
beforeEach(() => {
const acMock = jasmine.createSpyObj('AdminCommService', {
getMenu: of()
})
TestBed.configureTestingModule({
declarations: [MenuNewComponent],
imports: [MatTableModule, MatInputModule, MatDatepickerModule, BrowserAnimationsModule, ReactiveFormsModule, MatDialogModule, MatIconModule],
providers: [
{provide: DateAdapter, useClass: MomentDateAdapter},
{provide: MAT_DATE_LOCALE, useValue: "pl-PL"},
{provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS},
{provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: {useUtc: true}},
{provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection},
{provide: AdminCommService, useValue: acMock}
],
});
fixture = TestBed.createComponent(MenuNewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,126 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker';
import { Moment } from 'moment';
import { FDSelection } from 'src/app/fd.da';
import { Menu } from 'src/app/types/menu';
import { AdminCommService } from '../admin-comm.service';
import { MatTableDataSource } from '@angular/material/table';
import * as moment from 'moment';
import { MatDialog } from '@angular/material/dialog';
import { MenuUploadComponent } from './menu-upload/menu-upload.component';
import { Status } from 'src/app/types/status';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MenuAddComponent } from './menu-add/menu-add.component';
import { LocalStorageService } from 'src/app/services/local-storage.service';
@Component({
selector: 'app-menu-new',
templateUrl: './menu-new.component.html',
styleUrls: ['./menu-new.component.scss'],
providers: [
{provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: FDSelection}
]
})
export class MenuNewComponent {
dcols: string[] = ['day', 'sn', 'ob', 'kol']
dataSource: MatTableDataSource<Menu> = new MatTableDataSource<Menu>()
range = new FormGroup({
start: new FormControl<Moment|null>(null),
end: new FormControl<Moment|null>(null),
})
loading = false
public options: any;
constructor (private ac: AdminCommService, private dialog: MatDialog, private sb: MatSnackBar, readonly ls: LocalStorageService) {
moment.updateLocale('pl', {
weekdays: ["Niedziela", "Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota"]
})
}
print() {
this.ac.menu.print(this.range.value.start, this.range.value.end)?.subscribe((r) => {
if (r && r.length > 0) {
var mywindow = window.open(undefined, 'Drukowanie', 'height=400,width=400')
mywindow?.document.write(r)
mywindow?.print()
mywindow?.close()
}
})
}
addDate() {
this.dialog.open(MenuAddComponent).afterClosed().subscribe((data) => {
if (data) {
switch (data.type) {
case "day":
this.ac.menu.new.single(data.value).subscribe(s => this.refreshIfGood(s))
break;
case "week":
this.ac.menu.new.range(data.value.start, data.value.count).subscribe(s => this.refreshIfGood(s))
break;
case "file":
this.requestData()
break;
default:
break;
}
}
})
}
requestData() {
this.loading = true
this.ac.menu.getOpts().subscribe((o) => {
this.options = o;
})
this.ac.menu.getMenu(this.range.value.start, this.range.value.end)?.subscribe((data) => {
this.loading = false
this.dataSource.data = data.map((v) => {
let newMenu: Menu = {
...v,
day: moment.utc(v.day)
}
return newMenu
})
})
}
private refreshIfGood(s: Status) {
if (s.status.toString().match(/2\d\d/)) {
this.requestData()
}
}
activateUpload() {
this.dialog.open(MenuUploadComponent).afterClosed().subscribe((data) => {
if (data) {
this.requestData()
}
})
}
editSn(id: string) {
this.ac.menu.editSn(id, this.dataSource.data.find(v => v._id == id)!.sn).subscribe(s => this.refreshIfGood(s))
}
editOb(id: string) {
this.ac.menu.editOb(id, this.dataSource.data.find(v => v._id == id)!.ob).subscribe(s => this.refreshIfGood(s))
}
editKol(id: string) {
this.ac.menu.editKol(id, this.dataSource.data.find(v => v._id == id)?.kol).subscribe(s => this.refreshIfGood(s))
}
editTitle(id: string) {
this.ac.menu.editTitle(id, this.dataSource.data.find(v => v._id == id)?.dayTitle).subscribe(s => this.refreshIfGood(s))
}
getStat(day: moment.Moment, m: "ob" | "kol") {
this.ac.menu.stat(day, m).subscribe((s) => this.sb.open(`${s.y} / ${s.y+s.n} = ${((s.y/(s.y+s.n))*100).toFixed(2)}%`, "Zamknij", {duration: 2500}))
}
remove(id: string) {
this.ac.menu.rm(id).subscribe(s => this.refreshIfGood(s))
}
}

View File

@@ -0,0 +1,4 @@
:host {
margin: 8pt;
display: block;
}

View File

@@ -0,0 +1,29 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuUploadComponent } from './menu-upload.component';
import { AdminCommService } from '../../admin-comm.service';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
describe('MenuUploadComponent', () => {
let component: MenuUploadComponent;
let fixture: ComponentFixture<MenuUploadComponent>;
beforeEach(() => {
const acMock = jasmine.createSpyObj('AdminCommService', ['postMenu'])
TestBed.configureTestingModule({
declarations: [MenuUploadComponent],
providers: [
{provide: AdminCommService, useValue: acMock},
{provide: MatDialogRef, useValue: {}}
],
imports: [MatDialogModule]
});
fixture = TestBed.createComponent(MenuUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core';
import { AdminCommService } from '../../admin-comm.service';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-upload-edit',
templateUrl: './menu-upload.component.html',
styleUrls: ['./menu-upload.component.scss'],
})
export class MenuUploadComponent {
constructor(private ac:AdminCommService, public dialogRef: MatDialogRef<MenuUploadComponent>) {}
protected file: File | undefined;
onFileChange(event: Event) {
const file:File = (event.target as HTMLInputElement).files![0];
if (file) {
this.file = file
} else {
this.file = undefined
}
}
submit() {
this.ac.menu.postMenu(this.file!)?.subscribe((value) => {
this.dialogRef.close(value)
})
}
}

View File

@@ -1,10 +1,10 @@
:host {
padding: 8pt;
display: block;
padding: 8pt;
display: block;
}
form {
display: flex;
flex-direction: column;
align-items: stretch;
}
display: flex;
flex-direction: column;
align-items: stretch;
}

View File

@@ -1,41 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewPostComponent } from './edit-post.component'
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatInputModule } from '@angular/material/input'
import { ReactiveFormsModule } from '@angular/forms'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { NewPostComponent } from './edit-post.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
describe('NewPostComponent', () => {
let component: NewPostComponent
let fixture: ComponentFixture<NewPostComponent>
let component: NewPostComponent;
let fixture: ComponentFixture<NewPostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [NewPostComponent],
imports: [
MatDialogModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
BrowserAnimationsModule,
],
imports: [MatDialogModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, BrowserAnimationsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: {} },
],
})
fixture = TestBed.createComponent(NewPostComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
{provide: MatDialogRef, useValue: {}},
{provide: MAT_DIALOG_DATA, useValue: {}}
]
});
fixture = TestBed.createComponent(NewPostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,35 +1,31 @@
import { Component, Inject } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'
import { Component, Inject } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-edit-post',
templateUrl: './edit-post.component.html',
styleUrls: ['./edit-post.component.scss'],
standalone: false,
styleUrls: ['./edit-post.component.scss']
})
export class NewPostComponent {
form: FormGroup
constructor(
public dialogRef: MatDialogRef<NewPostComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
form: FormGroup;
constructor (public dialogRef: MatDialogRef<NewPostComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
if (data == null) {
data = {
title: '',
content: '',
title:"",
content:"",
}
}
this.form = new FormGroup({
title: new FormControl(data.title),
content: new FormControl(data.content),
content: new FormControl(data.content)
})
}
protected makePost() {
this.dialogRef.close({
title: this.form.get('title')?.value,
content: this.form.get('content')?.value,
content: this.form.get('content')?.value
})
}
}

View File

@@ -1,43 +1,30 @@
<button mat-raised-button (click)="newPost()" color="accent" class="newPost">Nowy post</button>
<div class="mainc">
@if (ac.state() != 2) {
<app-load-shade [state]="ac.state()" [error]="ac.error()" (refresh)="ac.refresh()"/>
}
@for (item of ac.news | async; track item) {
<mat-card>
<mat-card-header>
<button mat-raised-button (click)="newPost()" color="accent">Nowy post</button>
<mat-spinner *ngIf="loading"></mat-spinner>
<mat-card *ngFor="let item of news">
<mat-card-header>
<mat-card-title>{{item.title}}</mat-card-title>
@if (item.pinned) {
<mat-icon>push_pin</mat-icon>
}
</mat-card-header>
<mat-card-content [innerHTML]="item.formatted"></mat-card-content>
<mat-card-actions>
<mat-icon *ngIf="item.pinned">push_pin</mat-icon>
<mat-card-subtitle>{{item._id}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content [innerHTML]="item.formatted">
</mat-card-content>
<mat-card-actions>
<button mat-mini-fab (click)="editPost(item)"><mat-icon>edit</mat-icon></button>
<button mat-mini-fab (click)="pinToggle(item)"><mat-icon>push_pin</mat-icon></button>
@switch (item.visible) {
@case (true) {
<button mat-mini-fab (click)="visibleToggle(item)">
<mat-icon>visibility</mat-icon>
</button>
}
@default {
<button mat-mini-fab (click)="visibleToggle(item)" color="warn">
<mat-icon>visibility_off</mat-icon>
</button>
}
}
<button mat-mini-fab (click)="visibleToggle(item)">
<div [ngSwitch]="item.visible">
<mat-icon *ngSwitchCase="true">visibility</mat-icon>
<mat-icon *ngSwitchDefault>visibility_off</mat-icon>
</div>
</button>
<button mat-mini-fab (click)="delete(item._id)"><mat-icon>delete_forever</mat-icon></button>
</mat-card-actions>
<mat-card-footer>
<p>{{fullName(item)}} {{item.date | date:'d-LL-yyyy HH:mm'}}</p>
</mat-card-footer>
</mat-card>
} @empty {
<mat-card>
<p>
</mat-card-actions>
<mat-card-footer>
<p>{{item.date | date:'d-LL-yyyy HH:mm'}}</p>
</mat-card-footer>
</mat-card>
<mat-card *ngIf="news.length == 0">
<p>
Brak wiadomości.
</p>
</mat-card>
}
</div>
</p>
</mat-card>

View File

@@ -1,48 +1,38 @@
mat-card {
margin: 15px;
padding: 1ch;
margin: 15px;
padding: 1ch;
width: 100%;
}
mat-card-title {
font-size: 1.5rem;
font-size: 1.5rem;
}
mat-card-footer p {
font-size: 0.8rem;
color: #4a4a4a;
margin-bottom: 0;
text-align: end;
@media (prefers-color-scheme: dark) {
color: #999999;
}
font-size: 0.8rem;
color: #4a4a4a;
@media (prefers-color-scheme: dark) {
color: #999999
}
margin-bottom: 0;
text-align: end;
}
mat-card-content p {
white-space: pre-line;
white-space: pre-line;
}
mat-card p {
margin: 15px;
margin: 15px;
}
button {
margin-right: 4pt;
margin-right: 4pt;
}
:host {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
}
.mainc {
position: relative;
height: 100%;
width: 100%;
}
.newPost {
margin: 1ch;
padding: 8pt;
display: flex;
flex-direction: column;
align-items: center;
}

View File

@@ -1,33 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewsEditComponent } from './news-edit.component'
import { MatDialogModule } from '@angular/material/dialog'
import { MatSnackBarModule } from '@angular/material/snack-bar'
import { of } from 'rxjs'
import { MatCardModule } from '@angular/material/card'
import { NewsEditComponent } from './news-edit.component';
import { AdminCommService } from '../admin-comm.service';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { of } from 'rxjs';
import { MatCardModule } from '@angular/material/card';
xdescribe('NewsEditComponent', () => {
let component: NewsEditComponent
let fixture: ComponentFixture<NewsEditComponent>
describe('NewsEditComponent', () => {
let component: NewsEditComponent;
let fixture: ComponentFixture<NewsEditComponent>;
let acMock
beforeEach(() => {
acMock = {
news: {
getNews: jasmine.createSpy('getNews').and.returnValue(of([])),
},
getNews: jasmine.createSpy('getNews').and.returnValue(of([]))
}
}
TestBed.configureTestingModule({
declarations: [NewsEditComponent],
// providers: [{ provide: AdminCommService, useValue: acMock }],
imports: [MatDialogModule, MatSnackBarModule, MatCardModule],
})
fixture = TestBed.createComponent(NewsEditComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [MatDialogModule, MatSnackBarModule, MatCardModule]
});
fixture = TestBed.createComponent(NewsEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

View File

@@ -1,79 +1,71 @@
import { Component, OnInit } from '@angular/core'
import { MatDialog } from '@angular/material/dialog'
import { NewPostComponent } from './new-post/edit-post.component'
import { catchError, throwError } from 'rxjs'
import { MatSnackBar } from '@angular/material/snack-bar'
import { News } from 'src/app/types/news.model'
import { NewsEditService } from './news-edit.service'
import { Component, OnInit } from '@angular/core';
import { AdminCommService } from '../admin-comm.service';
import { MatDialog } from '@angular/material/dialog';
import { NewPostComponent } from './new-post/edit-post.component';
import { catchError, throwError } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { News } from 'src/app/types/news';
import { marked } from 'marked';
@Component({
selector: 'app-news-edit',
templateUrl: './news-edit.component.html',
styleUrls: ['./news-edit.component.scss'],
standalone: false,
styleUrls: ['./news-edit.component.scss']
})
export class NewsEditComponent implements OnInit {
news:Array<News & {formatted: string}> = new Array<News & {formatted: string}>
loading = true
constructor(
protected ac: NewsEditService,
private dialog: MatDialog,
private sb: MatSnackBar
) { }
constructor(private ac:AdminCommService, private dialog:MatDialog, private sb:MatSnackBar) {}
ngOnInit() {
this.ac.refresh()
this.loading = true
this.ac.news.getNews().subscribe(data => {
this.loading = false
this.news = data.map(v => {
var nd: News & {formatted: string} = {
...v,
formatted: marked.parse(v.content, {breaks: true}).toString()
}
return nd
})
})
}
newPost() {
this.dialog
.open(NewPostComponent, { width: '90vw' })
.afterClosed()
.subscribe(result => {
if (result == undefined) return
this.ac
.postNews(result.title, result.content)
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(data => {
if (data.status == 201) {
this.ngOnInit()
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
}
})
this.dialog.open(NewPostComponent, {width: "90vw"}).afterClosed().subscribe(result=> {
if (result == undefined) return
this.ac.news.postNews(result.title, result.content).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(() => new Error(err.message))
})).subscribe((data)=>{
if (data.status == 201) {
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
}
})
})
}
editPost(item: any) {
this.dialog
.open(NewPostComponent, { data: item, width: '90vh' })
.afterClosed()
.subscribe(result => {
if (result == undefined) return
this.ac
.updateNews(item._id, result.title, result.content)
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(data => {
if (data.status == 200) {
this.ngOnInit()
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
}
})
this.dialog.open(NewPostComponent, {data: item, width: "90vh"}).afterClosed().subscribe(result=>{
if (result == undefined) return
this.ac.news.updateNews(item._id, result.title, result.content).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 200) {
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
}
})
})
}
delete(id: string) {
this.ac.deleteNews(id).subscribe(data => {
this.ac.news.deleteNews(id).subscribe(data => {
if (data.status == 200) {
this.ngOnInit()
}
@@ -81,47 +73,29 @@ export class NewsEditComponent implements OnInit {
}
visibleToggle(item: any) {
this.ac
.toggleNews(item._id, item.visible)
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(data => {
if (data.status == 200) {
this.ngOnInit()
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
}
})
this.ac.news.toggleNews(item._id, item.visible).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 200) {
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
}
})
}
pinToggle(item: any) {
pinToggle(item:any) {
console.log(item.pinned)
this.ac
.togglePin(item._id, item.pinned)
.pipe(
catchError(err => {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
return throwError(() => new Error(err.message))
})
)
.subscribe(data => {
if (data.status == 200) {
this.ngOnInit()
} else {
this.sb.open('Wystąpił błąd. Skontaktuj się z obsługą programu.')
}
})
}
fullName(n: News): string {
const { author: { fname, surname, uname } } = n;
if (fname || surname) {
return [fname, surname].filter(Boolean).join(' ');
}
return uname;
this.ac.news.togglePin(item._id, item.pinned).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 200) {
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
}
})
}
}

View File

@@ -1,23 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { NewsEditService } from './news-edit.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('NewsEditService', () => {
let service: NewsEditService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(NewsEditService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,90 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, signal } from '@angular/core';
import { marked } from 'marked';
import { BehaviorSubject, catchError, map, of } from 'rxjs';
import { News } from 'src/app/types/news.model';
import { STATE } from 'src/app/types/state';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class NewsEditService {
private _news = new BehaviorSubject<(News & {formatted: string})[]>([])
public readonly news = this._news.asObservable()
private _state = signal(STATE.NOT_LOADED);
public readonly state = this._state.asReadonly();
private _error = signal<string | undefined>(undefined);
public readonly error = this._error.asReadonly();
constructor(private http: HttpClient) { }
public refresh() {
this.getNews()
}
private getNews() {
this._state.set(STATE.PENDING)
this.http.get
<News[]>
(environment.apiEndpoint + `/admin/news`, { withCredentials: true, })
.pipe(
catchError((err: Error) => {
this._state.set(STATE.ERROR)
this._error.set(err.message)
return of()
}),
map(i => {
return i.map(v => ({
...v,
formatted: marked.parse(v.content, { breaks: true }).toString()
}))
})
).subscribe(v => {
this._error.set(undefined)
this._news.next(v ?? [])
this._state.set(STATE.LOADED)
})
}
postNews(title: string, content: string) {
return this.http.post<any>(
environment.apiEndpoint + `/admin/news`,
{ title: title, content: content },
{ withCredentials: true }
)
}
deleteNews(id: string) {
return this.http.delete<any>(
environment.apiEndpoint + `/admin/news/${id}`,
{ withCredentials: true }
)
}
toggleNews(id: string, inverter: boolean) {
return this.putNews(id, { visible: !inverter })
}
togglePin(id: string, inverter: boolean) {
return this.putNews(id, { pinned: !inverter })
}
updateNews(id: string, title: string, content: string) {
return this.putNews(id, {
title: title,
content: content,
date: Date.now,
})
}
private putNews(id: string, update: object) {
return this.http.put<any>(
environment.apiEndpoint + `/admin/news/${id}`,
update,
{ withCredentials: true }
)
}
}

View File

@@ -1,48 +1,42 @@
<form [formGroup]="form" (ngSubmit)="submit()">
<div formGroupName="recp">
<mat-radio-group formControlName="type">
<mat-radio-button value="uid">
<mat-form-field>
<mat-label>Użytkownik</mat-label>
<app-user-search formControlName="uid" required/>
</mat-form-field>
</mat-radio-button>
<mat-radio-button value="room">
<mat-form-field>
<mat-label>Pokój</mat-label>
<input matInput type="text" formControlName="room">
</mat-form-field>
</mat-radio-button>
@if (ls.capCheck(8)) {
<mat-radio-button value="group">
<mat-form-field>
<mat-label>Grupa</mat-label>
<mat-select formControlName="group">
@for (item of groups; track item) {
<mat-option [value]="item._id">{{item.name}}</mat-option>
}
</mat-select>
</mat-form-field>
</mat-radio-button>
}
</mat-radio-group>
</div>
<mat-form-field>
<mat-label>Tytuł</mat-label>
<input matInput type="text" formControlName="title">
</mat-form-field>
<br>
<div formGroupName="recp">
<mat-radio-group formControlName="type">
<mat-radio-button value="uid">
<mat-form-field>
<mat-label>Użytkownik</mat-label>
<app-user-search formControlName="uid" required/>
</mat-form-field>
</mat-radio-button>
<mat-radio-button value="room">
<mat-form-field>
<mat-label>Pokój</mat-label>
<input matInput type="text" formControlName="room">
</mat-form-field>
</mat-radio-button>
<mat-radio-button value="group" *ngIf="ls.capCheck(8)">
<mat-form-field>
<mat-label>Grupa</mat-label>
<mat-select formControlName="group">
<mat-option *ngFor="let item of groups" [value]="item._id">{{item.name}}</mat-option>
</mat-select>
</mat-form-field>
</mat-radio-button>
</mat-radio-group>
</div>
<mat-form-field>
<mat-label>Zawartość wiadomości</mat-label>
<textarea matInput cdkTextareaAutosize formControlName="body"></textarea>
<mat-label>Tytuł</mat-label>
<input matInput type="text" formControlName="title">
</mat-form-field>
<br>
<button mat-fab extended type="submit">
<mat-form-field>
<mat-label>Zawartość wiadomości</mat-label>
<textarea matInput cdkTextareaAutosize formControlName="body"></textarea>
</mat-form-field>
<br>
<button mat-fab extended type="submit">
<mat-icon>send</mat-icon>
Wyślij
</button>
</form>
</button>
</form>
@if (success) {
<p>Udało się wysłać {{success.sent}} z {{success.possible}} = {{success.sent/success.possible | percent}}</p>
}
<p *ngIf="success">Udało się wysłać {{success.sent}} z {{success.possible}} = {{success.sent/success.possible | percent}}</p>

View File

@@ -1,3 +1,3 @@
mat-radio-button {
display: block;
}
display: block;
}

View File

@@ -1,59 +1,43 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationsComponent } from './notifications.component'
import { RouterModule } from '@angular/router'
import { MatRadioModule } from '@angular/material/radio'
import {
MatFormFieldControl,
MatFormFieldModule,
} from '@angular/material/form-field'
import { Component, forwardRef } from '@angular/core'
import { MatIconModule } from '@angular/material/icon'
import { Observable, of } from 'rxjs'
import {
AbstractControlDirective,
ControlValueAccessor,
FormsModule,
NG_VALUE_ACCESSOR,
NgControl,
ReactiveFormsModule,
} from '@angular/forms'
import { MatInputModule } from '@angular/material/input'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { NotificationsComponent } from './notifications.component';
import { AdminCommService } from '../admin-comm.service';
import { RouterModule } from '@angular/router';
import { MatRadioModule } from '@angular/material/radio';
import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field';
import { Component, forwardRef } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { Observable, of } from 'rxjs';
import { AbstractControlDirective, ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: 'app-user-search',
template: '',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserSearchStub),
multi: true,
},
{
provide: MatFormFieldControl,
useExisting: UserSearchStub,
},
],
standalone: false,
selector: "app-user-search", template: '', providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserSearchStub),
multi: true,
},
{
provide: MatFormFieldControl,
useExisting: UserSearchStub
}]
})
class UserSearchStub
implements ControlValueAccessor, MatFormFieldControl<never>
{
value: null = null
stateChanges: Observable<void> = of()
id: string = ''
placeholder: string = ''
ngControl: NgControl | AbstractControlDirective | null = null
focused: boolean = false
empty: boolean = true
shouldLabelFloat: boolean = true
required: boolean = false
disabled: boolean = false
errorState: boolean = false
controlType?: string | undefined
autofilled?: boolean | undefined
userAriaDescribedBy?: string | undefined
class UserSearchStub implements ControlValueAccessor, MatFormFieldControl<never> {
value: null = null;
stateChanges: Observable<void> = of();
id: string = "";
placeholder: string = "";
ngControl: NgControl | AbstractControlDirective | null = null;
focused: boolean = false;
empty: boolean = true;
shouldLabelFloat: boolean = true;
required: boolean = false;
disabled: boolean = false;
errorState: boolean = false;
controlType?: string | undefined;
autofilled?: boolean | undefined;
userAriaDescribedBy?: string | undefined;
setDescribedByIds(ids: string[]): void {}
onContainerClick(event: MouseEvent): void {}
writeValue(obj: any): void {}
@@ -62,19 +46,21 @@ class UserSearchStub
setDisabledState?(isDisabled: boolean): void {}
}
xdescribe('NotificationsComponent', () => {
let component: NotificationsComponent
let fixture: ComponentFixture<NotificationsComponent>
describe('NotificationsComponent', () => {
let component: NotificationsComponent;
let fixture: ComponentFixture<NotificationsComponent>;
beforeEach(() => {
const acMock = {
notif: {
getGroups: jasmine.createSpy('getGroups').and.returnValue(of()),
},
getGroups: jasmine.createSpy("getGroups").and.returnValue(of())
}
}
TestBed.configureTestingModule({
declarations: [NotificationsComponent, UserSearchStub],
// providers: [{ provide: AdminCommService, useValue: acMock }],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
RouterModule.forRoot([]),
MatRadioModule,
@@ -83,15 +69,15 @@ xdescribe('NotificationsComponent', () => {
FormsModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule,
],
})
fixture = TestBed.createComponent(NotificationsComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
NoopAnimationsModule
]
});
fixture = TestBed.createComponent(NotificationsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy()
})
})
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More