diff options
author | benal01 <bja4245@rit.edu> | 2025-04-05 13:22:58 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-05 13:22:58 -0400 |
commit | 0103ffc6f84d04433943c644ab759c1d04b5e681 (patch) | |
tree | 1cd8fb31b849b4d5be00d4be0e3db92bbbafb5e1 /ufund-ui | |
parent | 5a5d31896d79a736bce33b7d1aa7b3168ba308a9 (diff) | |
parent | 04db6f32b249ffb17c571cd6b16c8c54397f0be4 (diff) | |
download | JellySolutions-0103ffc6f84d04433943c644ab759c1d04b5e681.tar.gz JellySolutions-0103ffc6f84d04433943c644ab759c1d04b5e681.tar.bz2 JellySolutions-0103ffc6f84d04433943c644ab759c1d04b5e681.zip |
Merge pull request #27 from RIT-SWEN-261-02/need-list-abstraction
Need list abstraction
Diffstat (limited to 'ufund-ui')
31 files changed, 686 insertions, 708 deletions
diff --git a/ufund-ui/src/app/app.component.css b/ufund-ui/src/app/app.component.css index f4ed668..8b6b28a 100644 --- a/ufund-ui/src/app/app.component.css +++ b/ufund-ui/src/app/app.component.css @@ -4,6 +4,26 @@ height: 100%; } +#scroll { + overflow: auto; + min-height: 100%; + display: flex; + flex-direction: column; +} + +.modal { + position: absolute; + background-color: #000000a8; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 3; +} + #header { display: flex; flex-direction: row; diff --git a/ufund-ui/src/app/app.component.html b/ufund-ui/src/app/app.component.html index f697695..30a5347 100644 --- a/ufund-ui/src/app/app.component.html +++ b/ufund-ui/src/app/app.component.html @@ -1,17 +1,22 @@ -<div id="header"> - <div> - <a routerLink="/"> - <h1>Jelly Solutions</h1> - </a> +<div id="scroll"> + <div id="header"> + <div> + <a routerLink="/"> + <h1>Jelly Solutions</h1> + </a> + </div> + <div> + <a *ngIf="(currentUser | async)?.type === userType.MANAGER" routerLink="/dashboard">Dashboard</a> + <a routerLink="/cupboard">Cupboard</a> + <a *ngIf="(currentUser | async)?.type === userType.HELPER" routerLink="/basket">Basket</a> + <!-- <span>{{currentUser$ | async}}</span>--> + <button *ngIf="currentUser | async" (click)="logout()">Log Out</button> + <button *ngIf="!(currentUser | async)" (click)="login()">Log In</button> + </div> </div> - <div> - <a *ngIf="(currentUser | async)?.type === userType.MANAGER" routerLink="/dashboard">Dashboard</a> - <a routerLink="/cupboard">Cupboard</a> - <a *ngIf="(currentUser | async)?.type === userType.HELPER" routerLink="/basket">Basket</a> -<!-- <span>{{currentUser$ | async}}</span>--> - <button *ngIf="currentUser | async" (click)="logout()"> Log Out</button> - <button *ngIf="!(currentUser | async)" (click)="login()"> Log In</button> - </div> -</div> -<router-outlet /> + <router-outlet /> +</div> +<div *ngIf="modalService.getModalBehaviorSubject() | async" class="modal"> + <ng-container [ngTemplateOutlet]="modalService.getModalBehaviorSubject() | async" /> +</div> diff --git a/ufund-ui/src/app/app.component.ts b/ufund-ui/src/app/app.component.ts index bc0e71a..615397f 100644 --- a/ufund-ui/src/app/app.component.ts +++ b/ufund-ui/src/app/app.component.ts @@ -5,6 +5,7 @@ import {AuthService} from './services/auth.service'; import {ToastsService} from './services/toasts.service'; import {User, userType} from './models/User'; import {ActivatedRoute, Router} from '@angular/router'; +import {ModalService} from './services/modal.service'; @Component({ selector: 'app-root', @@ -22,6 +23,7 @@ export class AppComponent implements OnInit { private route: ActivatedRoute, protected toastService: ToastsService, private viewContainerRef: ViewContainerRef, + protected modalService: ModalService, @Inject(DOCUMENT) private document: Document ) {} diff --git a/ufund-ui/src/app/components/cupboard/cupboard.component.css b/ufund-ui/src/app/components/cupboard/cupboard.component.css index e45d929..9c37582 100644 --- a/ufund-ui/src/app/components/cupboard/cupboard.component.css +++ b/ufund-ui/src/app/components/cupboard/cupboard.component.css @@ -52,3 +52,36 @@ margin-top: 3px; } } + +#header2 { + display: flex; + flex-direction: column; + gap: 10px +} + +#searchArea { + display: flex; + + form { + display: flex; + width: 100%; + gap: 10px; + } + + input[type=text] { + display: flex; + width: 100%; + } +} + +#sortArea { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; +} + +select { + font-size: 14pt; + padding: 5px; +} diff --git a/ufund-ui/src/app/components/cupboard/cupboard.component.html b/ufund-ui/src/app/components/cupboard/cupboard.component.html index 37954bb..cd8fec2 100644 --- a/ufund-ui/src/app/components/cupboard/cupboard.component.html +++ b/ufund-ui/src/app/components/cupboard/cupboard.component.html @@ -1,38 +1,50 @@ <div id="box"> <div id="header"> <h1> Cupboard </h1> - <button *ngIf="isManager()" class="button2" (click)="this.selectForm('create')"><span class="icon">add</span>Create Need</button> + <ng-template #create> + <app-need-edit [mode]="'Create'" (refreshNeedList)="refresh()"></app-need-edit> + </ng-template> + <button *ngIf="usersService.isManager()" class="button2" (click)="modalService.showModal(create)"><span class="icon">add</span>Create Need</button> </div> - <app-need-list (currentNeed) = populateForm($event) #needList></app-need-list> -</div> -<ng-template [ngIf]="isManager()" > -<div> - <app-need-edit *ngIf="selectedForm === 'update'" [selectedNeed]="selectedNeed" (refreshNeedList)="needList.refresh()"></app-need-edit> - <div> - <div id="create-form" *ngIf="selectedForm === 'create'"> - <h1> Create Need </h1> - <form #cupboardForm="ngForm" (ngSubmit)="submit(cupboardForm.value)"> - <label>Name:</label><br> - <input type="text" name="name" ngModel><br> - <label>Image:</label><br> - <input type="text" name="image" ngModel><br> - <label>Location:</label><br> - <input type="text" name="location" ngModel><br> - <label>Max Goal:</label><br> - <input type="number" name="maxGoal" ngModel><br> - <label>Type</label><br> - <input type="radio" name="type" value="MONETARY" ngModel> - <label>Monetary</label><br> - <input type="radio" name="type" value="PHYSICAL" ngModel> - <label>Physical</label><br> - <input type="checkbox" name="urgent" value="false" ngModel> - <label>Urgent</label><br> - <label>Description</label><br> - <textarea name="description"></textarea><br> - <input type="submit" value="Submit"> - </form> - </div> + <div id="header2"> + <div id="searchArea"> + <form id="search-form" #searchForm="ngForm"> + <input type="text" name="search" class="wide-input" placeholder="Search in {{needs.length}} needs..." (input)="search(searchForm.value)" ngModel> + <input type="reset" value="Clear" (click)="search(null)"> <br> + </form> + </div> + <div id="sortArea"> + <label for="sort">Sort by: </label> + <select id='sort' [(ngModel)] = "currentSortAlgo" class="wide-input" (change)="search(searchForm.value)" [value]="currentSortAlgo"> + <option *ngFor="let algorithm of Object.keys(SortingAlgorithms)" [value]="algorithm"> + {{SortingAlgorithms[algorithm].display[sortMode === 'Ascending' ? 0 : 1]}} + </option> + </select> + <button (click)="toggleSortMode(searchForm.value)" [title]="sortMode"> + <span class="icon">{{sortMode === 'Ascending' ? 'arrow_upward': 'arrow_downward'}}</span> + </button> + <label>Needs per page: </label> + <input type ="number" [(ngModel)]="itemsPerPage" min="1" max="{{searchResults.length}}"> + </div> </div> + <h2 *ngIf="searchResults.length < needs.length && searchResults.length != 0"> Search Results({{needs.length - searchResults.length}} needs filtered): </h2> + <h2 *ngIf="searchResults.length == needs.length"> All Needs </h2> + <h2 *ngIf="searchResults.length == 0"> No Results Found </h2> + + <ng-template let-need #NLActions> + <button *ngIf="usersService.isHelper()" (click)="addToBasket(need)" [disabled]="usersService.inBasket(usersService.getBasket() | async, need)"> + <span class="icon">{{usersService.inBasket(usersService.getBasket() | async, need)? "check": "add" }}</span>Add To Basket + </button> + <ng-template #edit> + <app-need-edit *ngIf="need" [mode]="'Edit'" [need]="need" (refreshNeedList)="refresh()"></app-need-edit> + </ng-template> + <button *ngIf="usersService.isManager()" (click)="modalService.showModal(edit)"> + <span class="icon">edit</span>Edit Need + </button> + <button *ngIf="usersService.isManager()" (click)="deleteNeed(need.id)" > + <span class="icon">delete</span>Delete Need + </button> + </ng-template> + <app-need-list [actionArea]="NLActions" *ngIf="searchResults.length > 0" [needs]="searchResults" [itemsPerPage]="itemsPerPage" #needList/> </div> -</ng-template> diff --git a/ufund-ui/src/app/components/cupboard/cupboard.component.ts b/ufund-ui/src/app/components/cupboard/cupboard.component.ts index a4706b3..b03b77e 100644 --- a/ufund-ui/src/app/components/cupboard/cupboard.component.ts +++ b/ufund-ui/src/app/components/cupboard/cupboard.component.ts @@ -1,11 +1,14 @@ import {Component, OnInit, ViewChild} from '@angular/core'; -import { CupboardService } from '../../services/cupboard.service'; -import { Need } from '../../models/Need'; -import { userType } from '../../models/User'; -import { catchError, of } from 'rxjs'; -import { NeedListComponent } from '../need-list/need-list.component'; +import {CupboardService} from '../../services/cupboard.service'; +import {Need} from '../../models/Need'; +import {catchError, of} from 'rxjs'; +import {NeedListComponent} from '../need-list/need-list.component'; import {AuthService} from '../../services/auth.service'; import {ToastsService, ToastType} from '../../services/toasts.service'; +import {UsersService} from '../../services/users.service'; +import {SortingAlgoArrays} from './sorting'; +import {Router} from '@angular/router'; +import {ModalService} from '../../services/modal.service'; @Component({ selector: 'app-cupboard', @@ -13,111 +16,125 @@ import {ToastsService, ToastType} from '../../services/toasts.service'; templateUrl: './cupboard.component.html', styleUrl: './cupboard.component.css' }) - export class CupboardComponent implements OnInit { - selectedForm?: string = undefined; - needs: any; + // selectedForm?: string = undefined; + // needs: any; @ViewChild("needList") needList?: NeedListComponent + private searchDelay: any; + needs: Need[] = []; + searchResults: Need[] = []; + sortMode: 'Ascending' | 'Descending' = 'Ascending' + itemsPerPage = 5; + currentSortAlgo = 'sortByPriority'; + constructor( private cupboardService: CupboardService, private authService: AuthService, - private toastService: ToastsService + private toastService: ToastsService, + protected usersService: UsersService, + private router: Router, + protected modalService: ModalService ) {} ngOnInit(): void { - this.cupboardService.getNeeds().subscribe(n => this.needs = n); - if (this.isManager()) { - console.log("Admin view of Cupboard"); - } else { - console.log("Limited helper view of Cupboard"); - } + this.cupboardService.getNeeds().subscribe(n => { + this.needs = n; + // this.refresh() + this.search(null) + }); + this.authService.getCurrentUserSubject().subscribe( + () => this.usersService.refreshBasket()) + } + + refresh() { + this.cupboardService.getNeeds().subscribe(n => { + if (this.sortMode == 'Ascending') { + this.needs = n.sort(SortingAlgoArrays[this.currentSortAlgo].func); + } else { + this.needs = n.sort(SortingAlgoArrays[this.currentSortAlgo].func).reverse(); + } + this.searchResults = this.needs; + // this.updateVisibleNeeds(); + }); + + const form = document.getElementById('search-form') as HTMLFormElement; + form.reset(); + this.search(null); } - selectedNeed: any = { - name: '', - location:'', - id: null, - maxGoal: null, - type: '', - urgent: false - }; - selectedNeedId: number | null = null; - searchResults: any[] = []; + async search(form: any) { + console.log(this.currentSortAlgo) + //wait .25 seconds before searching but cancel if another search is made during the wait to prevent too many api calls - selectForm(name: string) { - //get search results from the need list - if (this.needList) { - this.searchResults = this.needList.searchResults; + //remove previous search if it exists + if (this.searchDelay) { + clearTimeout(this.searchDelay); } - console.log(this.searchResults) - this.selectedForm = name; - if (name == 'update') { - if (this.searchResults) { - this.searchResults.forEach((element: any) => { - console.log(element) - }); - } + if (form) { + this.searchDelay = setTimeout(() => { + + if (form) { + const currentSearchValue = form.search; //latest value of the search + this.cupboardService.searchNeeds(currentSearchValue).subscribe((n) => { + if (this.sortMode == 'Ascending') { + this.searchResults = n.sort(SortingAlgoArrays[this.currentSortAlgo].func); + } else { + this.searchResults = n.sort(SortingAlgoArrays[this.currentSortAlgo].func).reverse(); + } + // this.updateVisibleNeeds(); + }); + } + }, 250); + } else { + //user has cleared the search bar, we can skip the timeout for a 1/4 second faster response + //clear timeout to stop pending search + clearTimeout(this.searchDelay); + this.searchResults = this.needs; } } - async updateSearchResults() { - if (this.needList) { - while (this.selectedForm == 'update') { - this.searchResults = this.needList.searchResults - await new Promise(resolve => setTimeout(resolve, 100)); - } + toggleSortMode(form : any) { + if (this.sortMode == 'Ascending'){ + this.sortMode = 'Descending' + } else { + this.sortMode = 'Ascending' } + this.search(form) } - populateForm(need: any): void { - this.selectForm('update'); - this.selectedNeed = { ...need }; + deleteNeed(id : number) { + this.cupboardService.deleteNeed(id).subscribe(() => { + this.toastService.sendToast(ToastType.INFO, "Need deleted.") + this.needs = this.needs.filter(n => n.id !== id) + }) + this.refresh(); } - isManager() { - const type = this.authService.getCurrentUser()?.type; - return type === ("MANAGER" as unknown as userType); + addToBasket(need: Need) { + const currentUser = this.authService.getCurrentUser(); + //console.log("get current user in angular:", currentUser) + if (currentUser) { + if (!currentUser.basket.includes(need.id)) { + currentUser.basket.push(need.id); + this.usersService.updateUser(currentUser) + .pipe(catchError((err, _) => { + console.error(err); + return of(); + })) + .subscribe(() => { + let action = {label: "View Basket", onAction: () => this.router.navigate(['/basket'])} + this.toastService.sendToast(ToastType.INFO, `"${need.name}" Added to basket`, action) + this.usersService.refreshBasket(); + }); + } else { + this.toastService.sendToast(ToastType.ERROR, "This need is already in your basket!") + } + } } - submit(form: any) { - const need: Need = { - name: form.name, - image: form.image, - location: form.location, - id: 0, - maxGoal: form.maxGoal, - type: form.type, - urgent: !!form.urgent, - filterAttributes: [], - current: 0, - description: form.description - }; - console.log("need:", need); - console.log("form submitted. creating need: ", need); - this.cupboardService.createNeed(need) - .pipe(catchError((ex, _) => { - if (ex.status == 500) { - this.toastService.sendToast(ToastType.ERROR, "Fields cannot be blank"); - } else if (ex.status == 400) { - this.toastService.sendToast(ToastType.ERROR, ex.error); - } else { - this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); - } - return of() - })) - .subscribe( - (result) => { - if (result) { - console.log("need created successfully"); - this.needList?.refresh() - } else { - console.log("need creation failed"); - } - } - - ); - } + protected readonly SortingAlgorithms = SortingAlgoArrays; + protected readonly Object = Object; } diff --git a/ufund-ui/src/app/components/cupboard/sorting.ts b/ufund-ui/src/app/components/cupboard/sorting.ts new file mode 100644 index 0000000..5c37019 --- /dev/null +++ b/ufund-ui/src/app/components/cupboard/sorting.ts @@ -0,0 +1,69 @@ +import {Need} from '../../models/Need'; + +interface sortAlgo { + (a: Need, b: Need): number; +} + +// sort functions +const sortByName: sortAlgo = (a: Need, b: Need): number => { + if(a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { + return -1; + } + return 1; +} + +const sortByGoal: sortAlgo = (a: Need, b: Need): number => { + if(a.maxGoal == b.maxGoal) { + return sortByName(a,b); + } + else if(a.maxGoal > b.maxGoal) { + return -1; + } + return 1; +} + +const sortByCompletion: sortAlgo = (a: Need, b: Need): number => { + if(a.current == b.current) { + return sortByGoal(a,b); + } + else if(a.current > b.current) { + return -1; + } + return 1; +} + +const sortByPriority: sortAlgo = (a: Need, b: Need): number => { + if(a.urgent == b.urgent) { + return sortByGoal(a,b); + } + else if(a.urgent && !b.urgent) { + return -1; + } + return 1; +} + +const sortByLocation: sortAlgo = (a: Need, b: Need): number => { + if(a.location.toLocaleLowerCase() < b.location.toLocaleLowerCase()) { + return -1; + } + return 1; +} + +const sortByType: sortAlgo = (a:Need, b:Need): number => { + if(a.type == b.type) { + return sortByName(a,b); + } + else if(a.type > b.type) { + return -1; + } + return 1; +} + +export const SortingAlgoArrays: {[key: string]: { func: sortAlgo, display: [string, string]}} = { + sortByPriority: { func: sortByPriority, display: ["Highest Priority", "Lowest Priority" ] }, + sortByName: { func: sortByName, display: ["Name (A to Z)", "Name (Z to A)" ] }, + sortByLocation: { func: sortByLocation, display: ["Location (A to Z)", "Location (Z to A)" ] }, + sortByCompletion: { func: sortByCompletion, display: ["Most Completed", "Least Completed" ] }, + sortByGoal: { func: sortByGoal, display: ["Largest Maximum Goal", "Smallest Maximum Goal" ] }, + sortByType: { func: sortByType, display: ["Type (Physical first)", "Type (Monetary first)" ] }, +}; diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.css b/ufund-ui/src/app/components/dashboard/dashboard.component.css index 185fdc2..742a151 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.css +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.css @@ -1,7 +1,11 @@ :host { display: flex; - flex-direction: column; + justify-content: center; +} + +#box { + display: flex; width: 800px; - align-self: center; - gap: 20px + flex-direction: column; + gap: 10px; } diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.html b/ufund-ui/src/app/components/dashboard/dashboard.component.html index 2af467c..074ca76 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.html +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.html @@ -1,14 +1,17 @@ - -<h1>Admin Dashboard</h1> -<!--<app-mini-need-list [needList]="topNeeds" jtitle="Top needs" url="/cupboard"/>--> -<!--<app-mini-need-list [needList]="almostThere" jtitle="Almost there" url="/cupboard"/>--> -<!--<app-mini-need-list [needList]="inBasket" jtitle="In your basket" url="/basket"/>--> -<span>_ Registered users</span> -<span *ngIf="count"> {{count | async}} </span> -<span>_ Fulfilled needs</span> -<app-mini-need-list [needList]="fulfilledNeeds.getValue()" jtitle="Fulfilled needs"> </app-mini-need-list> -<span>_ Most fulfilled needs</span> -<app-mini-need-list [needList]="mostFulfilledNeeds.getValue()" jtitle="Most fulfilled"> </app-mini-need-list> -<span>_ Total monetary contributions</span> -<span *ngIf="totalDonations">${{totalDonations | async}} </span> -<span>_ </span> +<div id="box"> + @if ((authService.getCurrentUserSubject() | async)?.type === userType.MANAGER) { + <h1>Admin Dashboard</h1> + <span>_ Registered users</span> + <span *ngIf="count"> {{count | async}} </span> + <span>_ Fulfilled needs</span> + <app-mini-need-list [needList]="fulfilledNeeds.getValue()" label="Fulfilled needs"> </app-mini-need-list> + <span>_ Most fulfilled needs</span> + <app-mini-need-list [needList]="mostFulfilledNeeds.getValue()" label="Most fulfilled"> </app-mini-need-list> + <span>_ Total monetary contributions</span> + <span *ngIf="totalDonations">${{totalDonations | async}} </span> + <span>_ </span> + } @else { + <h1>Unauthorized</h1> + <span>This page requires you to be logged in as an admin! <a routerLink="/login">Log In</a></span> + } +</div> diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.ts b/ufund-ui/src/app/components/dashboard/dashboard.component.ts index 9bf7627..2ab4db2 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.ts +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.ts @@ -5,6 +5,7 @@ import {CupboardService} from '../../services/cupboard.service'; import {UsersService} from '../../services/users.service'; import {BehaviorSubject} from 'rxjs'; import {GoalType, Need} from '../../models/Need'; +import {userType} from '../../models/User'; @Component({ selector: 'app-dashboard', @@ -28,12 +29,6 @@ export class DashboardComponent implements OnInit{ ) {} ngOnInit() { - let user = this.authService.getCurrentUser() - if(!localStorage.getItem("credential") && !user) { - this.router.navigate(['/login']) - return - } - this.userService.getCount().subscribe(count => this.count.next(count)) this.cupboardService.getNeeds().subscribe(needs => { let totalValue = 0 @@ -55,4 +50,5 @@ export class DashboardComponent implements OnInit{ } + protected readonly userType = userType; } diff --git a/ufund-ui/src/app/components/funding-basket/funding-basket.component.html b/ufund-ui/src/app/components/funding-basket/funding-basket.component.html index bba66a3..3f840e1 100644 --- a/ufund-ui/src/app/components/funding-basket/funding-basket.component.html +++ b/ufund-ui/src/app/components/funding-basket/funding-basket.component.html @@ -1,44 +1,24 @@ <div id="box"> - <h1>Funding Basket</h1> - <ng-template [ngIf]="usersService.getBasket().getValue().length"> - <div id="needList"> - <div *ngFor="let need of usersService.getBasket().getValue()" class="needEntry"> - <div [routerLink]="'/need/' + need.id" class="clickable"> - <div class="split"> - <div class="left"> - <span class="needName">{{need.name}}</span> - <span class="needType">{{need.type}}</span> - </div> - - <div class="right"> - <span *ngIf="need.urgent" class="urgent">URGENT</span> - <span *ngIf="need.location"><span class="icon">location_on</span>{{need.location}}</span> - </div> - </div> - - <br> - - <div class="prog"> - <span id="hover-status-label-{{need.id}}"> </span> - <span>{{need.current}}/{{need.maxGoal}} ({{((need.current / need.maxGoal) * 100).toFixed(0)}}%)</span> - <progress [value]="need.current" [max]="need.maxGoal"></progress> - </div> - </div> - - <div class="actionArea"> - <input type="number" placeholder="Quantity" min="1" [id]="need.id" class="contribution" (input)="resetColor($event)"> - <button class="removeNeed" title="delete need" (click)="this.usersService.removeNeed(need.id)"> - <span class="icon">delete</span> Remove from Basket - </button> - </div> + @if ((authService.getCurrentUserSubject() | async)?.type === userType.HELPER) { + <h1>Funding Basket</h1> + <ng-template [ngIf]="(usersService.getBasket() | async)?.length"> + <ng-template let-need #NLActions> + <input type="number" placeholder="Quantity" min="1" [id]="need?.id" class="contribution" (input)="resetColor($event)"> + <button class="removeNeed" (click)="this.usersService.removeNeed(need.id)"> + <span class="icon">delete</span>Remove from Basket + </button> + </ng-template> + <app-need-list [actionArea]="NLActions" [needs]="(usersService.getBasket() | async)!"/> + <br> + <div id="footer"> + <button class="button2" title="checkout" (click)="checkout()">Checkout</button> </div> + </ng-template> + <div *ngIf="!usersService.getBasket().getValue().length"> + <span>There are no needs in your basket! </span><a routerLink="/cupboard">Browse the cupboard</a> </div> - <br> - <div id="footer"> - <button class="button2" title="checkout" (click)="checkout()">Checkout</button> - </div> - </ng-template> - <div *ngIf="!usersService.getBasket().getValue().length"> - <span>There are no needs in your basket! </span><a routerLink="/cupboard">Browse the cupboard</a> - </div> + } @else { + <h1>Unauthorized</h1> + <span>This page requires you to be logged in as a user! <a routerLink="/login">Log In</a></span> + } </div> diff --git a/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts b/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts index a39b4f3..371015a 100644 --- a/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts +++ b/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts @@ -2,9 +2,10 @@ import {Component, Input, OnInit, ViewChild} from '@angular/core'; import {UsersService} from '../../services/users.service'; import {Router} from '@angular/router'; import {CupboardService} from '../../services/cupboard.service'; -import {firstValueFrom} from 'rxjs'; +import {firstValueFrom, of} from 'rxjs'; import {AuthService} from '../../services/auth.service'; import {ToastsService, ToastType} from '../../services/toasts.service'; +import {userType} from '../../models/User'; @Component({ selector: 'app-funding-basket', @@ -18,18 +19,13 @@ export class FundingBasketComponent implements OnInit { private router: Router, protected cupboardService: CupboardService, protected usersService: UsersService, - private authService: AuthService, + protected authService: AuthService, private toastService: ToastsService ) {} @ViewChild("contribution") contribution?: Input; - // this is for login rerouting ngOnInit(): void { - if (!this.authService.getCurrentUser()) { - this.router.navigate(['/login'], {queryParams: {redir: this.router.url}}); - return; - } this.usersService.refreshBasket(); } @@ -63,9 +59,10 @@ export class FundingBasketComponent implements OnInit { } resetColor(ev: any) { - for (let contribution of document.querySelectorAll<HTMLInputElement>('.contribution')!) { - - } - (ev.target as HTMLInputElement).setAttribute("style", "border-color: unset") + // for (let contribution of document.querySelectorAll<HTMLInputElement>('.contribution')!) {} + (ev.target as HTMLInputElement).setAttribute("style", "") } + + protected readonly of = of; + protected readonly userType = userType; } diff --git a/ufund-ui/src/app/components/home-page/home-page.component.css b/ufund-ui/src/app/components/home-page/home-page.component.css index a10377f..b64be18 100644 --- a/ufund-ui/src/app/components/home-page/home-page.component.css +++ b/ufund-ui/src/app/components/home-page/home-page.component.css @@ -1,5 +1,5 @@ :host { - height: 100%; + flex-grow: 1; display: flex; flex-direction: column; align-items: center; diff --git a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css index 090bea9..d01ac54 100644 --- a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css +++ b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css @@ -45,6 +45,10 @@ cursor: pointer; } +.needEntry:hover { + background-color: #444444; +} + .needName { font-weight: bold; } diff --git a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html index 9febfa5..91f51cf 100644 --- a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html +++ b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html @@ -1,5 +1,5 @@ <div id="header"> - <span>{{jtitle}}</span> + <span>{{label}}</span> <a *ngIf="url" [routerLink]="url">Show All<span class="icon">arrow_forward_ios</span></a> </div> diff --git a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts index c909ae6..18b176b 100644 --- a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts +++ b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts @@ -10,7 +10,7 @@ import {Need} from '../../models/Need'; export class MiniNeedListComponent { @Input() needList?: Need[] - @Input() jtitle?: string + @Input() label?: string @Input() url?: string constructor( diff --git a/ufund-ui/src/app/components/need-edit/need-edit.component.css b/ufund-ui/src/app/components/need-edit/need-edit.component.css index 17605c2..b06e061 100644 --- a/ufund-ui/src/app/components/need-edit/need-edit.component.css +++ b/ufund-ui/src/app/components/need-edit/need-edit.component.css @@ -1,4 +1,4 @@ -:host { +#box { /*position: absolute;*/ /*background-color: rgba(0, 0, 0, 0.5);*/ /*display: flex;*/ @@ -8,14 +8,44 @@ /*right: 0;*/ /*z-index: 5;*/ /*justify-content: center;*/ + padding: 10px; + border-style: solid; + border-width: 1px; + border-color: #6c6c6c; + background-color: #2e2e2e; + border-radius: 5px; + position: relative; + width: 500px; } #create-form, #delete-form, #update-form { - margin-top: 50px; - background-color: #3a3a3a; - padding: 10px 20px 20px 20px; - border: 2px solid #000; - border-radius: 5px; + /*margin-top: 50px;*/ + + /*padding: 10px 20px 20px 20px;*/ + /*border: 2px solid #000;*/ + /*border-radius: 5px;*/ /*visibility: visible;*/ /*position: absolute;*/ + display: flex; + flex-direction: column; + gap: 10px; + + div { + display: flex; + flex-direction: column; + } +} + +#closeButton { + position: absolute; + width: 35px; + right: 10px; +} + +textarea { + height: 200px; +} + +label { + padding: 3px; } diff --git a/ufund-ui/src/app/components/need-edit/need-edit.component.html b/ufund-ui/src/app/components/need-edit/need-edit.component.html index e776415..ed4bfb3 100644 --- a/ufund-ui/src/app/components/need-edit/need-edit.component.html +++ b/ufund-ui/src/app/components/need-edit/need-edit.component.html @@ -1,23 +1,45 @@ -<div id="update-form"> - <h1> Update Need </h1> - <form #updateForm="ngForm" (ngSubmit)="update(updateForm.value)"> - <label>Name:</label><br> - <input type="text" name="name" [(ngModel)]="selectedNeed.name"><br> - <label>Image:</label><br> - <input type="text" name="image" [(ngModel)]="selectedNeed.image"><br> - <label>Location:</label><br> - <input type="text" name="location" [(ngModel)]="selectedNeed.location"><br> - <label>Max Goal:</label><br> - <input type="number" name="maxGoal" [(ngModel)]="selectedNeed.maxGoal"><br> - <label>Type</label><br> - <input type="radio" name="type" value="MONETARY" [(ngModel)]="selectedNeed.type"> - <label>Monetary</label><br> - <input type="radio" name="type" value="PHYSICAL" [(ngModel)]="selectedNeed.type"> - <label>Physical</label><br> - <input type="checkbox" name="urgent" [(ngModel)]="selectedNeed.urgent"> - <label>Urgent</label> <br> - <label>Description</label> <br> - <textarea name="description" [(ngModel)]="selectedNeed.description"></textarea><br> +<div id="box"> + <button id="closeButton" (click)="modalService.hideModal()"><span class="icon">close</span></button> + <h2>{{this.mode}} Need</h2> + <form #updateForm="ngForm" id="update-form" (ngSubmit)="submit(updateForm.value)"> + <div> + <span>Name:</span> + <input type="text" name="name" [(ngModel)]="needCopy.name"> + </div> + <div> + <span>Image:</span> + <input type="text" name="image" [(ngModel)]="needCopy.image"> + </div> + <div> + <span>Location:</span> + <input type="text" name="location" [(ngModel)]="needCopy.location"> + </div> + <div> + <span>Max Goal:</span> + <input type="number" name="maxGoal" [(ngModel)]="needCopy.maxGoal"> + </div> + <div> + <span>Type:</span> + <label> + <input type="radio" name="type" value="MONETARY" [(ngModel)]="needCopy.type"> + Monetary + </label> + <label> + <input type="radio" name="type" value="PHYSICAL" [(ngModel)]="needCopy.type"> + Physical + </label> + </div> + <div> + <span>Urgency:</span> + <label> + <input type="checkbox" name="urgent" [(ngModel)]="needCopy.urgent"> + Urgent + </label> + </div> + <div> + <span>Description:</span> + <textarea name="description" [(ngModel)]="needCopy.description"></textarea> + </div> <input type="submit" value="Submit"> </form> diff --git a/ufund-ui/src/app/components/need-edit/need-edit.component.ts b/ufund-ui/src/app/components/need-edit/need-edit.component.ts index 2462534..abfa543 100644 --- a/ufund-ui/src/app/components/need-edit/need-edit.component.ts +++ b/ufund-ui/src/app/components/need-edit/need-edit.component.ts @@ -1,61 +1,109 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { Need, GoalType } from '../../models/Need'; -import { CupboardService } from '../../services/cupboard.service'; -import { catchError, of } from 'rxjs'; -import { ToastsService, ToastType } from '../../services/toasts.service'; +import {Component, EventEmitter, Input, OnChanges, Output} from '@angular/core'; +import {GoalType, Need} from '../../models/Need'; +import {CupboardService} from '../../services/cupboard.service'; +import {catchError, of} from 'rxjs'; +import {ToastsService, ToastType} from '../../services/toasts.service'; +import {ModalService} from '../../services/modal.service'; +import {Router} from '@angular/router'; @Component({ - selector: 'app-need-edit', - standalone: false, - templateUrl: './need-edit.component.html', - styleUrl: './need-edit.component.css' + selector: 'app-need-edit', + standalone: false, + templateUrl: './need-edit.component.html', + styleUrl: './need-edit.component.css' }) -export class NeedEditComponent { - constructor( - private cupboardService: CupboardService, - private toastService: ToastsService - - ) {} - - @Input() selectedNeed!: Need; - @Output() refreshNeedList = new EventEmitter<void>(); - - update(form: any) { - console.log(form); - const need: Need = { - name: form.name, - image: form.image, - location: form.location, - id: this.selectedNeed.id, //system will control this - maxGoal: form.maxGoal, - type: GoalType[form.type as keyof typeof GoalType], - urgent: form.urgent, - filterAttributes: [], - current: 0, - description: form.description - }; - - this.cupboardService.updateNeed(need.id, need) - .pipe(catchError((ex, _) => { - if (ex.status == 500) { - this.toastService.sendToast(ToastType.ERROR, 'Fields cannot be blank'); - } else if (ex.status == 400) { - this.toastService.sendToast(ToastType.ERROR, ex.error); - } else { - this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); - } - return of() - })) - .subscribe( - (result) => { - if (result) { - console.log("need updated successfully"); - this.refreshNeedList.emit(); +export class NeedEditComponent implements OnChanges { + + @Input() mode!: "Create" | "Edit" + @Input() need?: Need; + @Output() refreshNeedList = new EventEmitter<void>(); + + needCopy: any = {} + + constructor( + private cupboardService: CupboardService, + private toastService: ToastsService, + protected modalService: ModalService, + private router: Router + ) {} + + ngOnChanges() { + this.needCopy = {...this.need} + } + + submit(form: any) { + const need: Need = { + name: form.name, + image: form.image, + location: form.location, + id: this.needCopy.id, //system will control this + maxGoal: form.maxGoal, + type: GoalType[form.type as keyof typeof GoalType], + urgent: form.urgent, + filterAttributes: [], + current: 0, + description: form.description + }; + + if (this.mode == "Edit") { + this.updateNeed(need) + } else if (this.mode === 'Create') { + this.createNeed(need) + } + } + + updateNeed(need: Need) { + this.cupboardService.updateNeed(need.id, need) + .pipe(catchError((ex, _) => { + if (ex.status == 500) { + this.toastService.sendToast(ToastType.ERROR, 'Fields cannot be blank'); + } else if (ex.status == 400) { + this.toastService.sendToast(ToastType.ERROR, ex.error); + } else { + this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); + } + return of() + })) + .subscribe( + (result) => { + if (result) { + let action = {label: 'View Need', onAction: () => this.router.navigate([`/need/${need.id}`])} + this.toastService.sendToast(ToastType.INFO, "Need updated successfully", action) + this.modalService.hideModal() + console.log("need updated successfully"); + this.refreshNeedList.emit(); + } else { + console.log("need update failed"); + } + } + ); + } + + createNeed(need: Need) { + this.cupboardService.createNeed(need) + .pipe(catchError((ex, _) => { + if (ex.status == 500) { + this.toastService.sendToast(ToastType.ERROR, "Fields cannot be blank"); + } else if (ex.status == 400) { + this.toastService.sendToast(ToastType.ERROR, ex.error); } else { - console.log("need update failed"); + this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); + } + return of() + })) + .subscribe( + (result) => { + if (result) { + let action = {label: 'View Need', onAction: () => this.router.navigate([`/need/${result.id}`])} + this.toastService.sendToast(ToastType.INFO, "Need created successfully", action) + this.modalService.hideModal() + console.log("need created successfully"); + this.refreshNeedList.emit(); + } else { + console.log("need creation failed"); + } } - } - ); - } -}
\ No newline at end of file + ); + } +} diff --git a/ufund-ui/src/app/components/need-list/need-list.component.css b/ufund-ui/src/app/components/need-list/need-list.component.css index e17609b..622b64a 100644 --- a/ufund-ui/src/app/components/need-list/need-list.component.css +++ b/ufund-ui/src/app/components/need-list/need-list.component.css @@ -1,9 +1,3 @@ -#header { - display: flex; - flex-direction: column; - gap: 10px -} - .needEntry { background-color: #2e2e2e; display: flex; @@ -17,33 +11,6 @@ gap: 15px } -select { - font-size: 14pt; - padding: 5px; -} - -#searchArea { - display: flex; - - form { - display: flex; - width: 100%; - gap: 10px; - } - - input[type=text] { - display: flex; - width: 100%; - } -} - -#sortArea { - display: flex; - flex-direction: row; - gap: 10px; - align-items: center; -} - .needName { font-weight: bold; } @@ -105,6 +72,10 @@ select { gap: 5px; } +.actionArea:empty { + padding: 0; +} + #page-selector { display: flex; align-items: center; diff --git a/ufund-ui/src/app/components/need-list/need-list.component.html b/ufund-ui/src/app/components/need-list/need-list.component.html index 84f80dc..406bfa0 100644 --- a/ufund-ui/src/app/components/need-list/need-list.component.html +++ b/ufund-ui/src/app/components/need-list/need-list.component.html @@ -1,30 +1,4 @@ -<div id="header"> - <div id="searchArea"> - <form id="search-form" #searchForm="ngForm"> - <input type="text" name="search" class="wide-input" placeholder="Search in {{needs.length}} needs..." (input)="search(searchForm.value)" ngModel> - <input type="reset" value="Clear" (click)="search(null)"> <br> - </form> - </div> - <div id="sortArea"> - <label for="sort">Sort by: </label> - <select id='sort' [(ngModel)] = "sortSelection" class="wide-input" (change)="search(searchForm.value)" [value]="sortSelection"> - <option *ngFor="let algorithm of SortingAlgoArrays" value="{{algorithm.name}}"> - {{algorithm.display[sortMode === 'Ascending' ? 0 : 1]}} - </option> - </select> - <button (click)="changeSortMode(searchForm.value)"> - <span class="icon">{{sortMode === 'Ascending' ? 'arrow_upward': 'arrow_downward'}}</span> - </button> - <label>Needs per page: </label> - <input type ="number" [(ngModel)]="itemsPerPage" (change)="editNeedsPerPage()" min="1" max="{{searchResults.length}}"> - </div> - <!--<button (click)="close()">Close</button>--> -</div> - <!-- display for when results are present and filtered--> -<h2 *ngIf="searchResults.length < needs.length && searchResults.length != 0"> Search Results({{needs.length - searchResults.length}} needs filtered): </h2> -<h2 *ngIf="searchResults.length == needs.length"> All Needs </h2> -<h2 *ngIf="searchResults.length == 0"> No Results Found </h2> <div id="needList"> <div *ngFor="let need of visibleNeeds" class="needEntry"> <div [routerLink]="'/need/' + need.id" class="clickable"> @@ -46,19 +20,9 @@ <span>{{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.current}}/{{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.maxGoal}} ({{((need.current / need.maxGoal) * 100).toFixed(0)}}%)</span> <progress [value]="need.current" [max]="need.maxGoal"></progress> </div> - </div> - - <div class="actionArea"> - <button *ngIf="isHelper()" (click)="add(need)"> - <span class="icon">add</span>Add To Basket - </button> - <button *ngIf="isManager()" (click)="select(need)"> - <span class="icon">edit</span>Edit Need - </button> - <button *ngIf="isManager()" (click)="delete(need.id)" > - <span class="icon">delete</span>Delete Need - </button> + <div *ngIf="actionArea" class="actionArea"> + <ng-container [ngTemplateOutlet]="actionArea" [ngTemplateOutletContext]="{$implicit: need}"/> </div> </div> </div> diff --git a/ufund-ui/src/app/components/need-list/need-list.component.ts b/ufund-ui/src/app/components/need-list/need-list.component.ts index ae6bc99..40af9f5 100644 --- a/ufund-ui/src/app/components/need-list/need-list.component.ts +++ b/ufund-ui/src/app/components/need-list/need-list.component.ts @@ -1,293 +1,58 @@ -import {Component, EventEmitter, Output} from '@angular/core'; +import {Component, Input, OnChanges, TemplateRef} from '@angular/core'; import {GoalType, Need} from '../../models/Need'; -import {CupboardService} from '../../services/cupboard.service'; -import {UsersService} from '../../services/users.service'; -import {userType} from '../../models/User'; -import {AuthService} from '../../services/auth.service'; -import {catchError, of} from 'rxjs'; -import {ToastsService, ToastType} from '../../services/toasts.service'; - -interface sortAlgo { - (a: Need,b: Need): number; -} - -// sort functions -const sortByName: sortAlgo = (a: Need, b: Need): number => { - if(a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { - return -1; - } - return 1; -} - -const sortByGoal: sortAlgo = (a: Need, b: Need): number => { - if(a.maxGoal == b.maxGoal) { - return sortByName(a,b); - } - else if(a.maxGoal > b.maxGoal) { - return -1; - } - return 1; -} - -const sortByCompletion: sortAlgo = (a: Need, b: Need): number => { - if(a.current == b.current) { - return sortByGoal(a,b); - } - else if(a.current > b.current) { - return -1; - } - return 1; -} - -const sortByPriority: sortAlgo = (a: Need, b: Need): number => { - if(a.urgent == b.urgent) { - return sortByGoal(a,b); - } - else if(a.urgent && !b.urgent) { - return -1; - } - return 1; -} - -const sortByLocation: sortAlgo = (a: Need, b: Need): number => { - if(a.location.toLocaleLowerCase() < b.location.toLocaleLowerCase()) { - return -1; - } - return 1; -} @Component({ - selector: 'app-need-list', - standalone: false, - templateUrl: './need-list.component.html', - styleUrl: './need-list.component.css' + selector: 'app-need-list', + standalone: false, + templateUrl: './need-list.component.html', + styleUrl: './need-list.component.css' }) -export class NeedListComponent { - selectedNeed: Need | null = null; - needs: Need[] = []; - searchResults: Need[] = []; - visibleNeeds: Need[] = []; - sortMode: 'Ascending' | 'Descending' = 'Ascending' - currentPage: number = 0; - itemsPerPage: number = 5; - totalPages: number = Math.ceil(this.needs.length / this.itemsPerPage); +export class NeedListComponent implements OnChanges { - getPrefix(need: Need) { - return (need.type === GoalType.MONETARY) ? "$" : ""; - } + @Input({required: true}) needs!: Need[] + @Input() itemsPerPage: number = 5; + @Input() actionArea: TemplateRef<any> | null = null - decrementPage() { - this.currentPage--; - this.updateVisibleNeeds(); - } + visibleNeeds: Need[] = []; + currentPage: number = 0; + totalPages: number = 0; - incrementPage() { - this.currentPage++; - this.updateVisibleNeeds(); - } + constructor( - lastPage() { - this.currentPage = this.totalPages - 1 - this.updateVisibleNeeds() - } + ) {} - firstPage() { - this.currentPage = 0 - this.updateVisibleNeeds() - } - - editNeedsPerPage() { - if (this.itemsPerPage > this.searchResults.length) { - this.itemsPerPage = this.searchResults.length; - } - if (this.itemsPerPage < 1) { - this.itemsPerPage = 1; + ngOnChanges() { + this.updateVisibleNeeds() } - this.resetVisibleNeeds(); - } - - updateVisibleNeeds() { - this.totalPages = Math.ceil(this.searchResults.length / this.itemsPerPage); - this.visibleNeeds = this.searchResults.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage); - } - - resetVisibleNeeds() { - this.currentPage = 0; - this.updateVisibleNeeds(); - } - currentSortAlgo: sortAlgo = sortByPriority; - sortSelection: string = 'sortByPriority'; - - SortingAlgoArrays: {func:sortAlgo,name:string, display:string[]}[] = [ - {func:sortByPriority,name:"sortByPriority", display:["Highest Priority", "Lowest Priority"]}, - {func:sortByName,name:"sortByName", display:["Name (A to Z)", "Name (Z to A)"]}, - {func:sortByLocation,name:"sortByLocation", display:["Location (A to Z)", "Location (Z to A)"]}, - {func:sortByCompletion,name:"sortByCompletion", display:["Most Completed", "Least Completed"]}, - {func:sortByGoal,name:"sortByGoal", display:["Largest Maximum Goal", "Smallest Maximum Goal"]}, - ]; - - @Output() currentNeed = new EventEmitter<Need>(); - - constructor( - private cupboardService: CupboardService, - private usersService: UsersService, - private authService: AuthService, - private toastService: ToastsService - ) {} - - refresh() { - this.cupboardService.getNeeds().subscribe(n => { - if (this.sortMode == 'Ascending') { - this.needs = n.sort(this.currentSortAlgo); - } else { - this.needs = n.sort(this.currentSortAlgo).reverse(); - } - this.searchResults = this.needs; - this.updateVisibleNeeds(); - }); - - const form = document.getElementById('search-form') as HTMLFormElement; - form.reset(); - this.search(null); + getPrefix(need: Need) { + return (need.type === GoalType.MONETARY) ? "$" : ""; } - ngOnInit(): void { - this.refresh() - - } - - changeSortMode(form : any) { - if (this.sortMode == 'Ascending'){ - this.sortMode = 'Descending' - } else { - this.sortMode = 'Ascending' + decrementPage() { + this.currentPage--; + this.updateVisibleNeeds(); } - this.search(form) - } - - private searchDelay: any; - async search(form: any) { - //wait .25 seconds before searching but cancel if another search is made during the wait to prevent too many api calls - - //remove previous search if it exists - if (this.searchDelay) { - clearTimeout(this.searchDelay); + incrementPage() { + this.currentPage++; + this.updateVisibleNeeds(); } - if (form) { - this.searchDelay = setTimeout(() => { - - if (form) { - //sorting based on algo selected - this.SortingAlgoArrays.forEach(algo => { - if(algo.name === this.sortSelection) { - this.currentSortAlgo = algo.func; - console.log("changed sorting algorithm to: ", algo.name + this.sortMode) - return - } - }); - - const currentSearchValue = form.search; //latest value of the search - this.cupboardService.searchNeeds(currentSearchValue).subscribe((n) => { - if (this.sortMode == 'Ascending') { - this.searchResults = n.sort(this.currentSortAlgo); - } else { - this.searchResults = n.sort(this.currentSortAlgo).reverse(); - } - this.updateVisibleNeeds(); - }); - } - }, 250); - } else { - //user has cleared the search bar, we can skip the timeout for a 1/4 second faster response - //clear timeout to stop pending search - clearTimeout(this.searchDelay); - this.searchResults = this.needs; - } - } - - delete(id : number) { - this.cupboardService.deleteNeed(id).subscribe(() => { - this.toastService.sendToast(ToastType.INFO, "Need deleted.") - this.needs = this.needs.filter(n => n.id !== id) - }) - this.refresh(); - } - isManager() { - const type = this.authService.getCurrentUser()?.type; - return type === ("MANAGER" as unknown as userType); - } - - isHelper() { - const type = this.authService.getCurrentUser()?.type; - return type === ("HELPER" as unknown as userType); - } - - changeText(id : number, text : string) { - const span = document.getElementById('hover-status-label-' + id); - if (span) { - span.innerHTML = ' ' + text; + lastPage() { + this.currentPage = this.totalPages - 1 + this.updateVisibleNeeds() } - } - add(need: Need) { - const currentUser = this.authService.getCurrentUser(); - //console.log("get current user in angular:", currentUser) - if (currentUser) { - if (!currentUser.basket.includes(need.id)) { - currentUser.basket.push(need.id); - this.toastService.sendToast(ToastType.INFO, "Need added to your basket!") - this.usersService.updateUser(currentUser) - .pipe(catchError((err, _) => { - console.error(err); - return of(); - })) - .subscribe(() => { - this.usersService.refreshBasket(); - }); - } else { - this.toastService.sendToast(ToastType.ERROR, "This need is already in your basket!") - } + firstPage() { + this.currentPage = 0 + this.updateVisibleNeeds() } - } - - back() { - this.searchResults = this.needs; - } - select(need : Need) { - //emit value - this.currentNeed.emit(need); - if (this.selectedNeed) { - //revert already selected need to previous style - console.log(need.id); - let button = document.getElementById('need-button-' + this.selectedNeed.id); - if (button) { - console.log(button) - button.style.background = 'lightgray'; - button.style.marginLeft = '0%'; - button.style.width = '98%'; - } - button = document.getElementById('need-edit-button-' + this.selectedNeed.id); - if (button) { - button.style.visibility = 'visible'; - } + updateVisibleNeeds() { + this.totalPages = Math.ceil(this.needs.length / this.itemsPerPage); + this.visibleNeeds = this.needs.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage); } - //change selected need to selected style - this.selectedNeed = need; - let button = document.getElementById('need-button-' + need.id); - if (button) { - button.style.background = 'white'; - button.style.marginLeft = '4%'; - button.style.width = '100%'; - } - button = document.getElementById('need-edit-button-' + need.id); - if (button) { - button.style.visibility = 'hidden'; - } - } protected readonly GoalType = GoalType; } diff --git a/ufund-ui/src/app/components/need-page/need-page.component.css b/ufund-ui/src/app/components/need-page/need-page.component.css index 44db4b4..f1b7f1f 100644 --- a/ufund-ui/src/app/components/need-page/need-page.component.css +++ b/ufund-ui/src/app/components/need-page/need-page.component.css @@ -66,8 +66,8 @@ .actionArea { display: flex; - padding: 5px; - gap: 5px; + padding: 5px 0; + gap: 10px; margin-top: 10px; } diff --git a/ufund-ui/src/app/components/need-page/need-page.component.html b/ufund-ui/src/app/components/need-page/need-page.component.html index 6921eac..ff5990f 100644 --- a/ufund-ui/src/app/components/need-page/need-page.component.html +++ b/ufund-ui/src/app/components/need-page/need-page.component.html @@ -1,51 +1,58 @@ <div id="box"> - <h1>{{need.name}}</h1> - <span class="needType">{{need.type}} GOAL</span> - <p>{{need.description}}</p> - <div class="prog"> -<!-- <span>{{need?.current}} / {{need?.maxGoal}}</span>--> - <progress [value]="need.current" [max]="need.maxGoal"></progress> - <span>This goal is <strong>{{(((need.current)*100) / (need.maxGoal)).toFixed(0)}}%</strong> complete!</span> - </div> - - - <div class="split"> - <div class="left"> - <span><strong>Target Goal:</strong> {{(need.type === GoalType.MONETARY) ? "$" : ""}}{{need.maxGoal}}</span> - - <span><strong>Amount Currently Collected:</strong> {{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.current}}</span> - - <span><strong>Location:</strong> {{need.location}}</span> - - <span><strong>Urgency: </strong> - <span *ngIf="!need.urgent">Not urgent</span> - <span *ngIf="need.urgent" class="urgent">URGENT</span> - </span> - - <div *ngIf="need.filterAttributes.length > 0"> - <strong>Tags:</strong> - <ul style="display: flex; column-gap: 24px;"> - <li *ngFor="let tag of need?.filterAttributes"> - <p>{{tag}}</p> - </li> - </ul> + @if (need) { + <h1>{{need.name}}</h1> + <span class="needType">{{need.type}} GOAL</span> + <p>{{need.description}}</p> + <div class="prog"> + <progress [value]="need.current" [max]="need.maxGoal"></progress> + <span>This goal is <strong>{{(((need.current)*100) / (need.maxGoal)).toFixed(0)}}%</strong> complete!</span> + </div> + + + <div class="split"> + <div class="left"> + <span><strong>Target Goal:</strong> {{(need.type === GoalType.MONETARY) ? "$" : ""}}{{need.maxGoal}}</span> + + <span><strong>Amount Currently Collected:</strong> {{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.current}}</span> + + <span><strong>Location:</strong> {{need.location}}</span> + + <span><strong>Urgency: </strong> + <span *ngIf="!need.urgent">Not urgent</span> + <span *ngIf="need.urgent" class="urgent">URGENT</span> + </span> + + <div *ngIf="need.filterAttributes?.length"> + <strong>Tags:</strong> + <ul style="display: flex; column-gap: 24px;"> + <li *ngFor="let tag of need?.filterAttributes"> + <p>{{tag}}</p> + </li> + </ul> + </div> + </div> + <div class="right"> + <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> </div> </div> - <div class="right"> - <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> + + + <div class="actionArea"> + <button *ngIf="usersService.isHelper()" (click)="add(need)" [disabled]="usersService.inBasket(usersService.getBasket() | async, need)"> + <span class="icon">{{usersService.inBasket(usersService.getBasket() | async, need)? "check": "add" }}</span>Add To Basket + </button> + <ng-template #edit> + <app-need-edit [mode]="'Edit'" *ngIf="need" [need]="need" (refreshNeedList)="ngOnInit()"></app-need-edit> + </ng-template> + <button *ngIf="usersService.isManager()" (click)="modalService.showModal(edit)"> + <span class="icon">edit</span>Edit Need + </button> + <button *ngIf="usersService.isManager()" (click)="delete(need!.id)" > + <span class="icon">delete</span>Delete Need + </button> </div> - </div> - - - <div class="actionArea"> - <button *ngIf="isHelper()" (click)="add(need!)"> - <span class="icon">add</span>Add To Basket - </button> -<!-- <button *ngIf="isManager()" (click)="edit(need!)">--> -<!-- <span class="icon">edit</span>Edit Need--> -<!-- </button>--> - <button *ngIf="isManager()" (click)="delete(need!.id)" > - <span class="icon">delete</span>Delete Need - </button> - </div> + } @else { + <h1>Need not found</h1> + <span>The requested need does not exist. <a routerLink="/cupboard">Browse the cupboard</a></span> + } </div> diff --git a/ufund-ui/src/app/components/need-page/need-page.component.ts b/ufund-ui/src/app/components/need-page/need-page.component.ts index ad4cacf..0967266 100644 --- a/ufund-ui/src/app/components/need-page/need-page.component.ts +++ b/ufund-ui/src/app/components/need-page/need-page.component.ts @@ -1,12 +1,12 @@ -import {Component, Input} from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import {GoalType, Need} from '../../models/Need'; import {ActivatedRoute, Router} from "@angular/router"; import {CupboardService} from "../../services/cupboard.service"; -import {userType} from '../../models/User'; import {AuthService} from '../../services/auth.service'; import {catchError, of} from 'rxjs'; import {ToastsService, ToastType} from '../../services/toasts.service'; import {UsersService} from '../../services/users.service'; +import {ModalService} from '../../services/modal.service'; @Component({ selector: 'app-need-page', @@ -14,18 +14,17 @@ import {UsersService} from '../../services/users.service'; templateUrl: './need-page.component.html', styleUrl: './need-page.component.css' }) -export class NeedPageComponent { +export class NeedPageComponent implements OnInit { constructor( private route: ActivatedRoute, private cupboardService: CupboardService, private authService: AuthService, - private usersService: UsersService, + protected usersService: UsersService, private toastService: ToastsService, - private router: Router + private router: Router, + protected modalService: ModalService ) {} - public GoalType = GoalType; - @Input() need!: Need; ngOnInit(): void { @@ -37,16 +36,6 @@ export class NeedPageComponent { window.history.back(); } - isManager() { - const type = this.authService.getCurrentUser()?.type; - return type === ("MANAGER" as unknown as userType); - } - - isHelper() { - const type = this.authService.getCurrentUser()?.type; - return type === ("HELPER" as unknown as userType); - } - add(need: Need) { const currentUser = this.authService.getCurrentUser(); //console.log("get current user in angular:", currentUser) @@ -69,19 +58,17 @@ export class NeedPageComponent { delete(id : number) { this.cupboardService.deleteNeed(id) - .pipe(catchError((ex, r) => { + .pipe(catchError((ex, _) => { this.toastService.sendToast(ToastType.ERROR, ex.error) return of() })) .subscribe(() => { // this.needs = this.needs.filter(n => n.id !== id) this.toastService.sendToast(ToastType.INFO, "Need deleted") - this.router.navigate(['/']) + this.router.navigate(['/cupboard']) }) // this.refresh(); } - edit(need: Need) { - - } + readonly GoalType = GoalType } diff --git a/ufund-ui/src/app/components/toast/toast.component.css b/ufund-ui/src/app/components/toast/toast.component.css index 4cd81fe..82e2ff3 100644 --- a/ufund-ui/src/app/components/toast/toast.component.css +++ b/ufund-ui/src/app/components/toast/toast.component.css @@ -14,7 +14,7 @@ animation: slideDown .5s ease-in-out; transition: transform .5s; align-self: center; - z-index: 3; + z-index: 4; position: absolute; top: 15px; display: flex; @@ -39,7 +39,7 @@ } } -.toast.hide { +.toast.hide:not(:has(:hover)) { transform: translateY(-90px); } diff --git a/ufund-ui/src/app/components/toast/toast.component.html b/ufund-ui/src/app/components/toast/toast.component.html index dccf869..dc33ecd 100644 --- a/ufund-ui/src/app/components/toast/toast.component.html +++ b/ufund-ui/src/app/components/toast/toast.component.html @@ -1,6 +1,6 @@ <div class="toast" [ngClass]="ToastType[type].toLowerCase()" #toastDiv> <span>{{this.message}}</span> - <a *ngIf="this.action" (click)="this.action.onAction()">{{this.action.label}}</a> + <a *ngIf="this.action" href="#" (click)="this.action.onAction(); $event.preventDefault(); hide()">{{this.action.label}}</a> <button (click)="hide()"> <span class="icon">close</span> </button> diff --git a/ufund-ui/src/app/services/cupboard.service.ts b/ufund-ui/src/app/services/cupboard.service.ts index 1060476..a87dee2 100644 --- a/ufund-ui/src/app/services/cupboard.service.ts +++ b/ufund-ui/src/app/services/cupboard.service.ts @@ -23,8 +23,8 @@ export class CupboardService { private authService: AuthService ) {} - createNeed(need: Need): Observable<boolean> { - return this.http.post<boolean>(this.url, need, this.httpOptions()) + createNeed(need: Need): Observable<Need> { + return this.http.post<Need>(this.url, need, this.httpOptions()) } getNeeds(): Observable<Need[]> { diff --git a/ufund-ui/src/app/services/modal.service.ts b/ufund-ui/src/app/services/modal.service.ts new file mode 100644 index 0000000..04f2f3a --- /dev/null +++ b/ufund-ui/src/app/services/modal.service.ts @@ -0,0 +1,25 @@ +import {Injectable, TemplateRef} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ModalService { + + private modal = new BehaviorSubject<TemplateRef<any> | null>(null) + + constructor() {} + + showModal(template: TemplateRef<any>) { + console.log("got here", template) + this.modal.next(template) + } + + hideModal() { + this.modal.next(null) + } + + getModalBehaviorSubject() { + return this.modal; + } +} diff --git a/ufund-ui/src/app/services/users.service.ts b/ufund-ui/src/app/services/users.service.ts index 080c394..35d080d 100644 --- a/ufund-ui/src/app/services/users.service.ts +++ b/ufund-ui/src/app/services/users.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; import {BehaviorSubject, catchError, firstValueFrom, Observable, of} from 'rxjs'; -import {User} from '../models/User'; +import {User, userType} from '../models/User'; import { Need } from '../models/Need'; import { CupboardService } from './cupboard.service'; import {AuthService} from './auth.service'; @@ -34,7 +34,9 @@ export class UsersService { private http: HttpClient, private cupboardService: CupboardService, private authService: AuthService - ) {} + ) { + authService.getCurrentUserSubject().subscribe(() => this.refreshBasket()) + } async createUser(username:string, password:string) { await firstValueFrom(this.http.post<User>(this.url, {username: username, password: password}, this.httpOptions())) @@ -84,4 +86,16 @@ export class UsersService { return this.basket; } + isManager() { + return this.authService.getCurrentUser()?.type === userType.MANAGER + } + + isHelper() { + return this.authService.getCurrentUser()?.type === userType.HELPER + } + + inBasket(basket: Need[] | null, need: Need) { + return basket?.map(r => r.id).includes(need.id); + } + } diff --git a/ufund-ui/src/styles.css b/ufund-ui/src/styles.css index 75d6b36..527408a 100644 --- a/ufund-ui/src/styles.css +++ b/ufund-ui/src/styles.css @@ -19,7 +19,9 @@ body { font-optical-sizing: auto; } -input { +input, textarea { + resize: none; + font-family: Inter, sans-serif; font-size: 14pt; padding: 5px; border-radius: 5px; @@ -42,6 +44,7 @@ button, input[type=button], input[type=reset], input[type=submit], .button { gap: 5px; text-align: center; justify-content: center; + align-items: center; &:hover { background-color: light-dark(#e1e1e1, #444444); |