diff options
Diffstat (limited to '')
35 files changed, 909 insertions, 856 deletions
diff --git a/ufund-ui/src/app/app.component.css b/ufund-ui/src/app/app.component.css index 6cb44f6..2ed8131 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 a6e6e6a..8a0fd04 100644 --- a/ufund-ui/src/app/app.component.html +++ b/ufund-ui/src/app/app.component.html @@ -1,18 +1,23 @@ -<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> +            <button *ngIf="(currentUser | async)?.type === userType.MANAGER" routerLink="/dashboard">Dashboard</button> +            <button routerLink="/cupboard">Cupboard</button> +            <button *ngIf="(currentUser | async)?.type === userType.HELPER" routerLink="/basket">Basket</button> +    <!--        <span>{{currentUser$ | async}}</span>--> +            <button *ngIf="currentUser | async" (click)="logout()">Log Out</button> +            <button *ngIf="!(currentUser | async)" (click)="login()">Log In</button> +            <button class="button" (click)="toggleColorScheme()"><span class="icon">brightness_7</span></button> +        </div>      </div> -    <div> -        <button *ngIf="(currentUser | async)?.type === userType.MANAGER" routerLink="/dashboard">Dashboard</button> -        <button routerLink="/cupboard">Cupboard</button> -        <button *ngIf="(currentUser | async)?.type === userType.HELPER" routerLink="/basket">Basket</button> -<!--        <span>{{currentUser$ | async}}</span>--> -        <button *ngIf="currentUser | async" (click)="logout()"> Log Out</button> -        <button *ngIf="!(currentUser | async)" (click)="login()"> Log In</button> -        <button class="button" (click)="toggleColorScheme()"><span class="icon">brightness_7</span></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 2fecc2d..78fd050 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..4eebc2d 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" (change)="editItemsPerPage()" 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 [uid]="0" [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..f571566 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,139 @@ 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 = parseInt(localStorage.getItem('itemsPerPage') ?? '5') ?? 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())      } -    selectedNeed: any = { -        name: '', -        location:'', -        id: null, -        maxGoal: null, -        type: '', -        urgent: false -    }; -    selectedNeedId: number | null = null; -    searchResults: any[] = []; - -    selectForm(name: string) { -        //get search results from the need list -        if (this.needList) { -            this.searchResults = this.needList.searchResults; -        } -        console.log(this.searchResults) -        this.selectedForm = name; -        if (name == 'update') { -            if (this.searchResults) { -                this.searchResults.forEach((element: any) => { -                    console.log(element) -                }); +    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);      } -    async updateSearchResults() { -        if (this.needList) { -            while (this.selectedForm == 'update') { -                this.searchResults = this.needList.searchResults -                await new Promise(resolve => setTimeout(resolve, 100)); -            } +    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 + +        //remove previous search if it exists +        if (this.searchDelay) { +            clearTimeout(this.searchDelay);          } -    } +        if (form) { +            this.searchDelay = setTimeout(() => { + +                if (form) { -    populateForm(need: any): void { -        this.selectForm('update'); -        this.selectedNeed = { ...need }; +                    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; +        }      } -    isManager() { -        const type = this.authService.getCurrentUser()?.type; -        return type === ("MANAGER" as unknown as userType); +    toggleSortMode(form : any) { +        if (this.sortMode == 'Ascending'){ +            this.sortMode = 'Descending' +        } else { +            this.sortMode = 'Ascending' +        } +        this.search(form)      } -    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) +    deleteNeed(id : number) { +        this.cupboardService.deleteNeed(id)              .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"); -                } +                this.toastService.sendToast(ToastType.ERROR, ex.error)                  return of()              })) -            .subscribe( -                (result) => { -                    if (result) { -                        console.log("need created successfully"); -                        this.needList?.refresh() -                    } else { -                        console.log("need creation failed"); -                    } -                } +            .subscribe(() => { +                this.toastService.sendToast(ToastType.INFO, "Need deleted.") +                this.refresh(); +            }) +    } + +    addToBasket(need: Need) { +        const currentUser = this.authService.getCurrentUser(); +        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!") +            } +        } +    } -            ); +    editItemsPerPage() { +        if (this.itemsPerPage > this.searchResults.length) { +            this.itemsPerPage = this.searchResults.length +        } +        if (this.itemsPerPage < 1) { +            this.itemsPerPage = 1 +        } +        localStorage.setItem('itemsPerPage', this.itemsPerPage.toString()) +        this.refresh();      } + +    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..54f362b 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.css +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.css @@ -1,7 +1,45 @@  :host {      display: flex; -    flex-direction: column; +    justify-content: center; +} + +#box { +    display: flex;      width: 800px; -    align-self: center; -    gap: 20px +    flex-direction: column; +    gap: 10px; +} + +#stats { +    display: flex; +    flex-direction: row; +    gap: 10px; +} + +.card { +    background-color: #2e2e2e; +    width: 400px; +    height: 130px; +    border-radius: 5px; +    padding: 10px; +    display: flex; +    flex-direction: column; +    justify-content: space-between; + +    h1 { +        padding: 0 10px; +    } +} + +.listCard { +    display: flex; +    flex-direction: column; +    background-color: #2e2e2e; +    border-radius: 5px; +    padding: 10px; +    gap: 10px; +} + +.content { +    align-self: end;  } diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.html b/ufund-ui/src/app/components/dashboard/dashboard.component.html index 2d7b4c3..233096a 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.html +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.html @@ -1,10 +1,29 @@ +<div id="box"> +    @if ((authService.getCurrentUserSubject() | async)?.type === userType.MANAGER) { +        <h1>Admin Dashboard</h1> +        <div id="stats"> +            <div class="card"> +                <span>Registered users</span> +                <h1 class="content" *ngIf="count"> {{count | async}} </h1> +            </div> -<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>_ Needs with overflow</span> -<span>_ Needs in peoples baskets</span> -<span>_ Total monetary contributions</span> -<span>_ </span> +            <div class="card"> +                <span>Total monetary contributions</span> +                <h1 class="content" *ngIf="totalDonations"> ${{totalDonations | async}} </h1> +            </div> +        </div> + +        <div class="listCard"> +            <span>Fulfilled needs</span> +            <app-mini-need-list [needList]="fulfilledNeeds.getValue()" label="Fulfilled needs"> </app-mini-need-list> +        </div> +        <div class="listCard"> +            <span>Most fulfilled needs</span> +            <app-mini-need-list [needList]="mostFulfilledNeeds.getValue()" label="Most fulfilled"> </app-mini-need-list> +        </div> + +    } @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 c94b5c6..2ab4db2 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.ts +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.ts @@ -1,10 +1,11 @@  import {Component, OnInit} from '@angular/core';  import {AuthService} from '../../services/auth.service';  import {Router} from '@angular/router'; -import {Need} from '../../models/Need';  import {CupboardService} from '../../services/cupboard.service'; -import {firstValueFrom} from 'rxjs';  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', @@ -14,9 +15,11 @@ import {UsersService} from '../../services/users.service';  })  export class DashboardComponent implements OnInit{ -    topNeeds?: Need[] -    almostThere?: Need[] -    inBasket?: Need[] +    protected count = new BehaviorSubject<number | undefined>(undefined) +    protected totalDonations = new BehaviorSubject<number | undefined>(undefined) +    protected totalNeeds = new BehaviorSubject<number | undefined>(undefined) +    protected fulfilledNeeds = new BehaviorSubject<Need[] | undefined>(undefined) +    protected mostFulfilledNeeds = new BehaviorSubject<Need[] | undefined>(undefined)      constructor(          protected authService: AuthService, @@ -26,20 +29,26 @@ export class DashboardComponent implements OnInit{      ) {}      ngOnInit() { -        let user = this.authService.getCurrentUser() -        if(!localStorage.getItem("credential") && !user) { -            this.router.navigate(['/login']) -            return -        } - -        firstValueFrom(this.cupboardService.getNeeds()).then(r => { -            this.topNeeds = r.sort((a, b) => b.current - a.current) -            this.almostThere = r.sort((a, b) => a.current/a.maxGoal - b.current/b.maxGoal) -        }) +        this.userService.getCount().subscribe(count => this.count.next(count)) +        this.cupboardService.getNeeds().subscribe(needs => { +            let totalValue = 0 +            for (let need of needs) { +                if (need.type === GoalType.MONETARY) { +                    totalValue += need.current +                    this.totalDonations.next(totalValue) +                } + +            } +            this.fulfilledNeeds.next(needs.filter(a => ((a.current / a.maxGoal)) >= 1)) +            needs.sort((a, b) => b.current/b.maxGoal - a.current/a.maxGoal) -        this.userService.getBasket().subscribe(r => { -            this.inBasket = r; +            needs = needs.filter(a => a.current != 0) +            this.totalNeeds.next(needs.length) +            this.mostFulfilledNeeds.next(needs.slice(0, 5))          }) + +      } +    protected readonly userType = userType;  } diff --git a/ufund-ui/src/app/components/funding-basket/funding-basket.component.css b/ufund-ui/src/app/components/funding-basket/funding-basket.component.css index bd41fda..4764b0f 100644 --- a/ufund-ui/src/app/components/funding-basket/funding-basket.component.css +++ b/ufund-ui/src/app/components/funding-basket/funding-basket.component.css @@ -80,3 +80,11 @@      padding: 5px;      gap: 5px;  } + +#footer { +    display: flex; +    flex-direction: row; +    align-items: center; +    gap: 20px; +    margin-bottom: 10px; +} 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 b88ef31..7158194 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,81 +1,25 @@ - -<!--<div id="needCount">--> -<!--    <label for="needCount">Needs in Basket:</label>--> -<!--    <span>{{ this.usersService.getBasket().getValue().length }}</span>--> -<!--</div>--> - -<!--<div *ngIf="this.usersService.getBasket().getValue().length == 0">--> -<!--    <h2>There are no needs in the basket</h2>--> -<!--</div>--> - -<!--<table class="needs" id="funding-basket" *ngIf="this.usersService.getBasket().getValue().length != 0">--> -<!--    <thead>--> -<!--        <tr>--> -<!--            <th class="need"></th>--> -<!--        </tr>--> -<!--    </thead>--> -<!--    <tbody>--> -<!--        <tr *ngFor="let need of usersService.getBasket().getValue()">--> -<!--            <td>--> -<!--                <a routerLink="/need/{{need.id}}">{{need.name}}</a>--> -<!--                <p>Goal: {{need.maxGoal}}</p>--> -<!--                <p>Current: {{(need.current).toFixed(2)}}</p>--> -<!--                <p>How much to Contribute: <input type="number" placeholder="insert value" min="1" id={{need.id}} class="contribution"></p>--> -<!--                <br>--> -<!--                <div>--> -<!--                    <button type="button" class="removeNeed" title="delete need"--> -<!--                    (click)="this.usersService.removeNeed(need.id)">Remove Need</button>--> -<!--                </div>--> -<!--            </td>--> -<!--        </tr>--> -<!--    </tbody>--> -<!--</table>--> -<!--<br>-->  <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 class="description">--> -                    <!--                {{need.description}}--> -                    <!--            </div>--> -                </div> - -                <div class="actionArea"> -                    <input type="number" placeholder="Quantity" min="1" id={{need.id}} class="contribution sort-scheme"> -                    <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 [uid]="1" [actionArea]="NLActions" [needs]="(usersService.getBasket() | async)!"/> +            <br> +            <div id="footer"> +                <button class="button2" title="checkout" (click)="checkout()">Checkout</button> +                <span id="running-total">Your current running total is: ${{runningTotal | async}}</span>              </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 847baee..78ce958 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,11 @@ 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 {catchError, firstValueFrom, Observable} from 'rxjs'; +import {BehaviorSubject, firstValueFrom, of} from 'rxjs';  import {AuthService} from '../../services/auth.service';  import {ToastsService, ToastType} from '../../services/toasts.service'; +import {userType} from '../../models/User'; +import {GoalType} from '../../models/Need';  @Component({      selector: 'app-funding-basket', @@ -18,77 +20,61 @@ export class FundingBasketComponent implements OnInit {          private router: Router,          protected cupboardService: CupboardService,          protected usersService: UsersService, -        private authService: AuthService, +        protected authService: AuthService,          private toastService: ToastsService      ) {} +    public runningTotal = new BehaviorSubject(0)      @ViewChild("contribution") contribution?: Input; -    @Input() isValid: boolean = true; -    // this is for login rerouting      ngOnInit(): void { -        if (!this.authService.getCurrentUser()) { -            this.router.navigate(['/login'], {queryParams: {redir: this.router.url}}); -            return; -        }          this.usersService.refreshBasket(); -        // this.usersService.removeNeed(); <- call this to remove      }      async checkout() { -        this.isValid = true; -        for (let c of document.querySelectorAll('.contribution')!) { -            let contribution = c as HTMLInputElement; -            contribution.setAttribute("style", ""); -            if (contribution.value == '' || contribution.valueAsNumber <= 0) { -                this.isValid = false; +        let order: { needID: number, quantity: number }[] = [] +        let isNotValid = false +        for (let contribution of document.querySelectorAll<HTMLInputElement>('.contribution')!) { +            if (contribution.value == '' || contribution.valueAsNumber <= 0) { +                isNotValid = true                  contribution.setAttribute("style", "border-color: #ff0000"); -                this.toastService.sendToast(ToastType.ERROR, "Invalid input in funding basket!") - -                setTimeout(() => { -                    contribution.setAttribute("style", "border-color: #ffffff"); -                }, 3000);              } +            order.push({needID: +contribution.id, quantity: contribution.valueAsNumber});          } -        // if (this.usersService.getBasket().value != await firstValueFrom(this.usersService.getUser(1)) -        // for (let c of this.usersService.getBasket().value) { -        //     if (c == null) { -        //         this.isValid = false; -        //         this.statusText.next("One or more needs have been deleted") -        //     } else { -        //         this.statusText.next("test") -        //     } -        // } -        if (this.isValid) { -            for (let c of document.querySelectorAll('.contribution')!) { -                let contribution = c as HTMLInputElement; -                let need = await firstValueFrom(this.cupboardService.getNeed(+contribution.id)); -                need.current += +contribution.value; -                this.usersService.removeNeed(+need.id); -                this.cupboardService.checkoutNeed(need.id, +contribution.value) -                    .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 new Observable<string>(); -                    })) -                    .subscribe((result) => { -                        if (result) { -                            //this.needList?.refresh() -                        } else { -                            console.log('need update failed'); -                        } -                        this.toastService.sendToast(ToastType.INFO, "Checkout successful"); -                    }); -            } + +        if (isNotValid) { +            this.toastService.sendToast(ToastType.ERROR, "Invalid input in funding basket!") +            return; +        } + +        try { +            await firstValueFrom(this.cupboardService.checkoutNeed(order)) +        } catch (ex:any) { +            this.toastService.sendToast(ToastType.ERROR, ex.error); +            return          } + +        order.forEach(contribution => this.usersService.removeNeed(contribution.needID)) +        this.toastService.sendToast(ToastType.INFO, "Checkout successful");      } +    resetColor(ev: any) { +        let total = 0 +        this.runningTotal.next(total); +        for (let contribution of document.querySelectorAll<HTMLInputElement>('.contribution')!) { +            this.cupboardService.getNeed(+contribution.id).subscribe(need => { +                if (contribution.value != '' && need.type != GoalType.PHYSICAL) { +                    total += contribution.valueAsNumber +                } +                this.runningTotal.next(total); +            }) +        } + +        (ev.target as HTMLInputElement).setAttribute("style", "border-color: unset") +    } +    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 f6e5631..47e6ebd 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 ac456ab..6dceee1 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 @@ -1,29 +1,24 @@  :host {      display: flex;      flex-direction: column; -    border: solid rgba(255, 255, 255, 0.5) 1px;      border-radius: 5px; +    height: 175px;  } -#header { +#empty { +    height: 175px;      display: flex; -    flex-direction: row; -    justify-content: space-between; -    border-bottom: solid rgba(255, 255, 255, 0.5) 1px; -    padding: 10px; - -    a { -        display: flex; -    } +    align-items: center; +    color: #878787;  }  #needList {      display: flex;      flex-direction: row; -    padding: 10px; +    /*padding: 10px;*/      gap: 10px;      justify-content: start; -    overflow: clip; +    overflow: auto;  }  .needEntry { @@ -45,6 +40,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 a2de9e5..fc70c3d 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,9 +1,7 @@ -<div id="header"> -    <span>{{jtitle}}</span> -    <a [routerLink]="url">Show All<span class="icon">arrow_forward_ios</span></a> -</div> -  <div id="needList"> +    <div id="empty" *ngIf="!needList?.length"> +        <span>(No needs)</span> +    </div>      <div class="needEntry" *ngFor="let need of needList" [routerLink]="'/need/'+need.id">          <div>              <span class="needName">{{need.name}}</span> 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 56ae6a6..b3af85f 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: var(--tertiary-color);      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;  } @@ -53,18 +20,49 @@ select {      font-size: 10pt;  } +.need-image { +    transition: all 0.15s ease-in-out; +    height: 130px; +    width: 200px; +    margin: -10px 0 0 -10px; +    object-fit: cover; +    border-radius: 5px; +    mask-image: linear-gradient(to right, rgb(255,255,255) 0, rgb(255,255,255,.1) 60%, rgb(255,255,255,0) 100%); +} + +/*.clickable:hover {*/ +/*    .need-image {*/ +/*        mask-image: none;*/ +/*        width: 210px*/ +/*    }*/ +/*    .left {*/ +/*        width: 32.5%;*/ +/*    }*/ +/*    .prog {*/ +/*        width: 72.5%;*/ +/*    }*/ +/*}*/ +  .split {      display: flex;      flex-direction: row; -    justify-content: space-between; -      .left { +        width: 15%; +        transition: all 0.2s ease-in-out; +        display: flex; +        flex-direction: column; +    } + +    .middle { +        width: 42.5%;          display: flex; +        align-items: start;          flex-direction: column;      }      .right { +        width: 42.5%;          display: flex;          flex-direction: column;          align-items: end; @@ -80,8 +78,12 @@ select {  }  .prog { +    transition: all 0.2s ease-in-out; +    width: 85%;      display: flex;      flex-direction: column; +    align-self: end; +    margin-top: -5.25%;  }  .clickable { @@ -105,6 +107,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 e9f70f6..0e5b762 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,39 +1,17 @@ -<div id="header"> -    <div id="searchArea"> -        <form id="search-form" #searchForm="ngForm"> -            <input type="text" name="search" class="wide-input sort-scheme" 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 [(ngModel)] = "sortSelection" class="wide-input sort-scheme" (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 class="sort-scheme" 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"> +              <div class="split">                  <div class="left"> +                    <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> +                </div> + +                <div class="middle">                      <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> @@ -46,19 +24,10 @@                  <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..7ca0ae7 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,62 @@ -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); - -  getPrefix(need: Need) { -      return (need.type === GoalType.MONETARY) ? "$" : ""; -  } +export class NeedListComponent implements OnChanges { -  decrementPage() { -    this.currentPage--; -    this.updateVisibleNeeds(); -  } +    @Input({required: true}) needs!: Need[] +    @Input({required: true}) uid!: number +    @Input() itemsPerPage: number = 5; +    @Input() actionArea: TemplateRef<any> | null = null -  incrementPage() { -    this.currentPage++; -    this.updateVisibleNeeds(); -  } +    visibleNeeds: Need[] = []; +    currentPage: number = parseInt(localStorage.getItem('currentPage'+this.uid) ?? '0') ?? 0; +    totalPages: number = 0; -  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; +    ngOnChanges() { +        this.updateVisibleNeeds() +        this.currentPage = parseInt(localStorage.getItem('currentPage'+this.uid) ?? '0') ?? 0;      } -    if (this.itemsPerPage < 1) { -      this.itemsPerPage = 1; -    } -    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' +    //increment/decrement +    decrementPage() { +        this.currentPage--; +        localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); +        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++; +        localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); +        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; +    //skipping pages +    lastPage() { +        this.currentPage = this.totalPages - 1 +        localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); +        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 +        localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); +        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 47aa8b3..6ca1350 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 @@ -4,11 +4,13 @@  }  #box { +    /*padding-top: 7.5%;*/      display: flex;      flex-direction: column;      width: 800px;      justify-content: start;      gap: 10px; +    padding: 0 10px;  }  .needName { @@ -22,32 +24,19 @@      /*margin-bottom: 20px;*/  } -.split { -    display: flex; -    flex-direction: row; -    justify-content: space-between; - - -    .left { -        display: flex; -        flex-direction: column; -        width : 50%; -    } - -    .right { -        display: flex; -        flex-direction: column; -        align-items: end; -    } -}  .need-image { -    width: 400px; -    height: auto; +    width: calc(100% + 40px); +    height: 40%; +    /*position: absolute;*/ +    left: 22.5%;      aspect-ratio: 16/9;      object-fit: cover; +    mask-image: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(255,255,255,.2) 80%, rgba(255,255,255,.1) 90%, transparent 100%);      border-radius: 10px; -    box-shadow: var(--dark-highlight-clor) 0 0 50px; +    margin-left: -20px; +    margin-right: -20px; +    margin-bottom: -80px;  }  .urgent { @@ -66,8 +55,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..2629346 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,50 @@  <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> - +    @if (need) { +        <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> +        <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 > 0"> -                <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"/> +        <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="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> +        <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> +    } @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..17e330c 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 { @@ -33,29 +32,15 @@ export class NeedPageComponent {          this.cupboardService.getNeed(id).subscribe(n => this.need = n);      } -    back() { -        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)          if (currentUser) {              if (!currentUser.basket.includes(need.id)) {                  currentUser.basket.push(need.id);                  this.usersService.updateUser(currentUser)                      .pipe(catchError((err, _) =>  { -                        console.error(err); +                        let action = {label: "View Basket", onAction: () => this.router.navigate(['/basket'])} +                        this.toastService.sendToast(ToastType.INFO, `"${need.name}" Added to basket`, action)                          return of();                      }))                      .subscribe(() => { @@ -69,19 +54,16 @@ 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.toastService.sendToast(ToastType.INFO, "Need deleted.") +                this.router.navigate(['/cupboard'])              })          // this.refresh();      } -    edit(need: Need) { - -    } +    readonly GoalType = GoalType  } diff --git a/ufund-ui/src/app/components/signup/signup.component.css b/ufund-ui/src/app/components/signup/signup.component.css index 429bc42..aa90e04 100644 --- a/ufund-ui/src/app/components/signup/signup.component.css +++ b/ufund-ui/src/app/components/signup/signup.component.css @@ -3,15 +3,26 @@      align-items: center;      justify-content: center;      height: 100%; -    margin-top: -66px +    margin-top: -66px; +    background: rgba(0, 0, 0, .65) url("https://4kwallpapers.com/images/walls/thumbs_2t/13136.png"); +    background-blend-mode: darken; +    background-size: cover;  }  #box {      display: flex;      flex-direction: column; -    /*max-width: 300px;*/ +    max-width: 500px;      gap: 10px; +    backdrop-filter: blur(25px); +    background-color: rgba(0, 0, 0, 0.1); +    padding: 30px; +    color: white; +    border-radius: 5px; +    border-style: solid; +    border-width: 1px; +    border-color: rgb(140, 140, 255);      & > div {          display: flex; @@ -19,6 +30,11 @@      }  } +#password { +    border-bottom-left-radius: 0; +    border-bottom-right-radius: 0; +} +  .border {      border-style: solid;      border-width: 1px; @@ -34,6 +50,8 @@      width: 100%;      appearance: none;      overflow: hidden; +    border-bottom-right-radius: 5px; +    border-bottom-left-radius: 5px;      /*margin-top: -5px;*/  } @@ -57,13 +75,10 @@  #passReq {      display: flex;      flex-direction: column; +    margin-top: 10px;  }  #box > div { -    display: flex; -    flex-direction: row; -    align-items: start; -    gap: 20px;      div {          display: flex; diff --git a/ufund-ui/src/app/components/signup/signup.component.html b/ufund-ui/src/app/components/signup/signup.component.html index 84f15e4..ef2fc27 100644 --- a/ufund-ui/src/app/components/signup/signup.component.html +++ b/ufund-ui/src/app/components/signup/signup.component.html @@ -7,13 +7,12 @@      <div>          <div> -            <input placeholder="Password" type="password" (input)="validate(username.value, confirmPass.value, password.value)" #password> +            <input id="password" placeholder="Password" type="password" (input)="validate(username.value, confirmPass.value, password.value)" #password>              <progress [ngClass]="'color' + strength.getValue()" id="bar" [value]="strength | async" max="5">  </progress>              <span *ngIf="passwordStatusText">{{passwordStatusText | async}}</span> -        </div> - -        <div id="passReq"> -            <span *ngFor="let requirement of Object.values(passwordRequirements)" [style.color]="requirement.value ? 'green' : 'red'"><span class="icon">{{requirement.value?"check":"close"}}</span> {{requirement.title}}</span> +            <div id="passReq"> +                <span *ngFor="let requirement of Object.values(passwordRequirements)" [style.color]="requirement.value ? 'green' : 'red'"><span class="icon">{{requirement.value?"check":"close"}}</span> {{requirement.title}}</span> +            </div>          </div>      </div> diff --git a/ufund-ui/src/app/components/signup/signup.component.ts b/ufund-ui/src/app/components/signup/signup.component.ts index 9c37211..2762d03 100644 --- a/ufund-ui/src/app/components/signup/signup.component.ts +++ b/ufund-ui/src/app/components/signup/signup.component.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {Component, ElementRef, ViewChild} from '@angular/core';  import {UsersService} from '../../services/users.service';  import {Router} from '@angular/router';  import {BehaviorSubject} from 'rxjs'; @@ -29,6 +29,7 @@ export class SignupComponent {      protected ableToCreateAccount = new BehaviorSubject(false)      protected passwordRequirements: PasswordRequirements = new PasswordRequirements()      protected strength = new BehaviorSubject(0) +    @ViewChild("username") usernameInput!: ElementRef<HTMLInputElement>      constructor(          protected usersService: UsersService, @@ -56,11 +57,11 @@ export class SignupComponent {      validate(username: string, passConfirm:string, password: string) {          this.passwordsMatch.next(false) -        this.usernameStatusText.next("") +        this.usernameInput.nativeElement.setAttribute("style", "")          this.checkPasswordStrength(password);          if (username === "") { -            this.usernameStatusText.next("Username field can't be blank") +            this.usernameInput.nativeElement.setAttribute("style", "border-color: #ff0000")          }          if (passConfirm && password === passConfirm) { @@ -105,8 +106,6 @@ export class SignupComponent {              this.passwordStatusText.next("")          } else if (strength == 0) {              this.passwordStatusText.next("") -        } else { -            this.passwordStatusText.next("Password must meet requirements")          }          this.strength.next(strength) 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/components/toast/toast.component.ts b/ufund-ui/src/app/components/toast/toast.component.ts index 47fd7ff..6bbae34 100644 --- a/ufund-ui/src/app/components/toast/toast.component.ts +++ b/ufund-ui/src/app/components/toast/toast.component.ts @@ -21,7 +21,6 @@ export class ToastComponent implements OnInit{      }      hide() { -        console.log(this.toastDiv, typeof this.toastDiv)          this.toastDiv.nativeElement.classList.add('hide')      } diff --git a/ufund-ui/src/app/services/cupboard.service.ts b/ufund-ui/src/app/services/cupboard.service.ts index 9232c0c..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[]> { @@ -47,7 +47,7 @@ export class CupboardService {          return this.http.delete<boolean>(`${this.url}/${id}`, this.httpOptions())      } -    checkoutNeed(id: number, quantity: number) { -        return this.http.put(`${this.url}/checkout`, {needID: id, amount: quantity}, this.httpOptions()) +    checkoutNeed(data: {needID: number, quantity: number}[]) { +        return this.http.put(`${this.url}/checkout`, data, this.httpOptions())      }  } 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 4080ebf..688d6e4 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'; @@ -21,11 +21,22 @@ export class UsersService {          })      }); +    httpOptions2 = () => ({ +        headers: new HttpHeaders({ +            'Content-Type': 'application/json', +            "jelly-api-key": this.authService.getApiKey() +        }), +        responseType: "text" as "json" // don't ask me how or why this works, bc i have no clue... +        // see the relevant angular bug report https://github.com/angular/angular/issues/18586 +    }); +      constructor(          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())) @@ -35,6 +46,10 @@ export class UsersService {          return this.http.get<User>(`${this.url}/${id}`, this.httpOptions())      } +    getCount(): Observable<number> { +        return this.http.get<number>(`${this.url}/count`, this.httpOptions2()) +    } +      updateUser(user: User): Observable<User> {          console.log(`${this.url}/${user.username}`, user, this.httpOptions)          return this.http.put<User>(`${this.url}/${user.username}`, user, this.httpOptions()) @@ -45,7 +60,9 @@ export class UsersService {      }      refreshBasket() { -        let promiseArr = this.authService.getCurrentUser()!.basket.map(async needID => { +        let usr = this.authService.getCurrentUser(); +        if (!usr) return; +        let promiseArr = usr.basket.map(async needID => {              return await firstValueFrom(this.cupboardService.getNeed(needID));          })          Promise.all(promiseArr).then(r => this.basket.next(r)); @@ -71,4 +88,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); +    } +  }  | 
