diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..1348ba4a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/angular.json b/angular.json index f98c9205..8d5cec9e 100644 --- a/angular.json +++ b/angular.json @@ -10,13 +10,14 @@ "prefix": "app", "schematics": { "@schematics/angular:component": { - "styleext": "scss" + "style": "scss" } }, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { + "sourceMap": true, "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", @@ -31,7 +32,12 @@ ], "styles": [ "src/styles.scss", - "src/assets/scss/argon.scss" + "src/assets/scss/argon.scss", + "src/assets/vendor/nucleo/css/nucleo.css", + "src/assets/vendor/@fortawesome/fontawesome-free/css/all.min.css", + "node_modules/ngx-toastr/toastr.css", + "node_modules/ngx-spinner/animations/ball-scale-multiple.css" + ], "scripts": [ "node_modules/chart.js/dist/Chart.min.js", @@ -40,7 +46,8 @@ }, "configurations": { "production": { - "optimization": { + "optimization": + { "scripts": true, "styles": { "minify": false, @@ -49,7 +56,7 @@ "fonts": true }, "outputHashing": "all", - "sourceMap": false, + "sourceMap": true, "namedChunks": false, "extractLicenses": true, "vendorChunk": false, @@ -62,34 +69,26 @@ ] }, "development": { + "buildOptimizer": false, + "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, - "namedChunks": true, - "buildOptimizer": false, - "optimization": { - "scripts": true, - "styles": { - "minify": false, - "inlineCritical": true - }, - "fonts": true - }, - "outputHashing": "all" + "namedChunks": true } } }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "browserTarget": "argon-dashboard-angular:build" + "buildTarget": "argon-dashboard-angular:build" }, "configurations": { "production": { - "browserTarget": "argon-dashboard-angular:build:production" + "buildTarget": "argon-dashboard-angular:build:production" }, "development": { - "browserTarget": "argon-dashboard-angular:build:development" + "buildTarget": "argon-dashboard-angular:build:development" } }, "defaultConfiguration": "development" @@ -97,7 +96,7 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "argon-dashboard-angular:build" + "buildTarget": "argon-dashboard-angular:build" } }, "test": { @@ -159,10 +158,9 @@ } } }, - "defaultProject": "argon-dashboard-angular", "schematics": { "@schematics/angular:component": { - "styleext": "scss" + "style": "scss" } }, "cli": { diff --git a/docs/argon.css b/docs/argon.css index 9ddd4bd1..ff9989c0 100644 --- a/docs/argon.css +++ b/docs/argon.css @@ -3575,7 +3575,8 @@ fieldset:disabled a.btn background-color: transparent; background-image: none; } -.btn-outline-info:hover +.btn-outline-info:hover, +.btn-outline-info.active { color: #fff; border-color: #11cdef; @@ -6254,6 +6255,11 @@ input[type='button'].btn-block background-color: #0da5c0; } +/* .badge-info.active { + color: #fff !important; + background-color: #0da5c0 !important; +} */ + .badge-warning { color: #ff3709; diff --git a/package.json b/package.json index 89497054..58421e9b 100644 --- a/package.json +++ b/package.json @@ -5,47 +5,57 @@ "ng": "ng", "start": "ng serve", "build": "cross-env CI=false ng build", + "build:prod": "ng build --prod", + "build:dev": "ng build --configuration development", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", - "install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install && npm start" + "install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install --force && npm start" }, "private": true, "dependencies": { - "@angular/animations": "^14.2.0", - "@angular/cdk": "^14.2.0", - "@angular/common": "^14.2.0", - "@angular/compiler": "^14.2.0", - "@angular/core": "^14.2.0", - "@angular/elements": "^14.2.0", - "@angular/forms": "^14.2.0", - "@angular/google-maps": "^14.2.0", - "@angular/localize": "^14.2.0", - "@angular/material": "^14.2.0", - "@angular/platform-browser": "^14.2.0", - "@angular/platform-browser-dynamic": "^14.2.0", - "@angular/router": "^14.2.0", - "@ng-bootstrap/ng-bootstrap": "12.0.1", + "@angular/animations": "^18.2.2", + "@angular/cdk": "^18.2.2", + "@angular/common": "^18.2.2", + "@angular/compiler": "^18.2.2", + "@angular/core": "^18.2.2", + "@angular/elements": "^18.2.2", + "@angular/fire": "^18.0.1", + "@angular/forms": "^18.2.2", + "@angular/google-maps": "^18.2.2", + "@angular/localize": "^18.2.2", + "@angular/material": "^18.2.2", + "@angular/platform-browser": "^18.2.2", + "@angular/platform-browser-dynamic": "^18.2.2", + "@angular/router": "^18.2.2", + "@ng-bootstrap/ng-bootstrap": "^17.0.1", "@popperjs/core": "^2.11.4", - "bootstrap": "4.6.1", + "bootstrap": "^4.6.2", "chart.js": "2.9.4", "clipboard": "2.0.10", + "firebase": "^10.13.1", + "lodash": "^4.17.21", "ngx-clipboard": "15.0.1", - "ngx-toastr": "14.2.2", + "ngx-spinner": "^17.0.0", + "ngx-toastr": "^19.0.0", "nouislider": "15.5.1", - "rxjs": "~7.5.0", - "zone.js": "~0.11.4", - "web-animations-js": "2.3.2" + "rxjs": "~7.8.0", + "sass": "^1.77.8", + "sass-loader": "^16.0.1", + "web-animations-js": "2.3.2", + "zone.js": "~0.14.10" }, "devDependencies": { - "@angular-devkit/build-angular": "^14.2.7", - "@angular/cli": "~14.2.7", - "@angular/compiler-cli": "^14.2.0", - "@angular/language-service": "14.2.0", + "@angular-devkit/build-angular": "^18.2.2", + "@angular/cli": "~18.2.2", + "@angular/compiler-cli": "^18.2.2", + "@angular/language-service": "18.2.2", "@types/jasmine": "~4.0.0", "@types/jasminewd2": "~2.0.10", - "@types/node": "^17.0.21", - "codelyzer": "6.0.2", + "@types/lodash": "^4.17.10", + "@types/node": "^22.5.2", + "codelyzer": "^6.0.2", + "cross-env": "^7.0.3", "jasmine-core": "~4.4.0", "jasmine-spec-reporter": "~7.0.0", "karma": "~6.4.0", @@ -54,9 +64,8 @@ "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.0.0", - "protractor": "7.0.0", + "protractor": "^7.0.0", "ts-node": "~10.9.1", - "typescript": "~4.7.2", - "cross-env": "^7.0.3" + "typescript": "^5.5.4" } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0a8b2ec9..71cbcbbb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,5 +6,5 @@ import { Component } from '@angular/core'; styleUrls: ['./app.component.scss'] }) export class AppComponent { - title = 'argon-dashboard-angular'; + title = 'Blu Inventory'; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 80a35bab..f8dc9c68 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,17 +1,25 @@ -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { HttpClientModule } from '@angular/common/http'; -import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { HttpClientModule } from "@angular/common/http"; +import { RouterModule } from "@angular/router"; -import { AppComponent } from './app.component'; -import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component'; -import { AuthLayoutComponent } from './layouts/auth-layout/auth-layout.component'; +import { AppComponent } from "./app.component"; +import { AdminLayoutComponent } from "./layouts/admin-layout/admin-layout.component"; +import { AuthLayoutComponent } from "./layouts/auth-layout/auth-layout.component"; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; -import { AppRoutingModule } from './app.routing'; -import { ComponentsModule } from './components/components.module'; +import { AppRoutingModule } from "./app.routing"; +import { ComponentsModule } from "./components/components.module"; +import { environment } from "../environments/environment"; +import { AngularFireModule } from "@angular/fire/compat"; +import { AngularFireAuthModule } from "@angular/fire/compat/auth"; +import { AngularFirestoreModule } from "@angular/fire/compat/firestore"; +import { ToastrModule } from "ngx-toastr"; +import { StudentLayoutComponent } from "./layouts/student-layout/student-layout.component"; +import { CustodianLayoutComponent } from "./layouts/custodian-layout/custodian-layout.component"; +import { NgxSpinnerModule } from "ngx-spinner";import { AccountantLayoutComponent } from './layouts/accountant-layout/accountant-layout.component'; @NgModule({ @@ -22,14 +30,23 @@ import { ComponentsModule } from './components/components.module'; ComponentsModule, NgbModule, RouterModule, - AppRoutingModule + AppRoutingModule, + AngularFireModule.initializeApp(environment.firebaseConfig), + AngularFireAuthModule, + AngularFirestoreModule, + ToastrModule.forRoot(), + NgxSpinnerModule.forRoot(), ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ AppComponent, AdminLayoutComponent, - AuthLayoutComponent + AuthLayoutComponent, + StudentLayoutComponent, + CustodianLayoutComponent, + AccountantLayoutComponent, ], providers: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 29a17f41..8a150011 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -1,3 +1,4 @@ +import { CustodianLayoutModule } from './layouts/custodian-layout/custodian-layout.module'; import { NgModule } from '@angular/core'; import { CommonModule, } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; @@ -5,33 +6,78 @@ import { Routes, RouterModule } from '@angular/router'; import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component'; import { AuthLayoutComponent } from './layouts/auth-layout/auth-layout.component'; +import { AuthGuard } from './guards/auth.guard'; +import { StudentLayoutComponent } from './layouts/student-layout/student-layout.component'; +import { LoginGuard } from './guards/login.guard'; +import { ItemComponent } from './components/cards/item/item.component'; +import { CustodianLayoutComponent } from './layouts/custodian-layout/custodian-layout.component'; +import { CheckoutComponent } from './pages/student/checkout/checkout.component'; +import { AccountantLayoutComponent } from './layouts/accountant-layout/accountant-layout.component'; const routes: Routes =[ { path: '', - redirectTo: 'dashboard', + redirectTo: 'auth/login', pathMatch: 'full', }, { - path: '', + path: 'admin', component: AdminLayoutComponent, + canActivate: [AuthGuard], children: [ { path: '', loadChildren: () => import('src/app/layouts/admin-layout/admin-layout.module').then(m => m.AdminLayoutModule) } ] - }, { - path: '', + }, + { + path: 'student', + component: StudentLayoutComponent, + canActivate: [AuthGuard], // Add role guard to check if the user is a student + children: [ + { + path: '', + loadChildren: () => import('./layouts/student-layout/student-layout.module').then(m => m.StudentLayoutModule) + } + ] + }, + { + path: 'custodian', + component: CustodianLayoutComponent, + canActivate: [AuthGuard], // Add role guard to check if the user is a student + children: [ + { + path: '', + loadChildren: () => import('./layouts/custodian-layout/custodian-layout.module').then(m => m.CustodianLayoutModule) + } + ] + }, + { + path: 'accountant', + component: AccountantLayoutComponent, + canActivate: [AuthGuard], // Add role guard to check if the user is a student + children: [ + { + path: '', + loadChildren: () => import('./layouts/accountant-layout/accountant-layout.module').then(m => m.AccountantLayoutModule) + } + ] + }, + { + path: 'auth', component: AuthLayoutComponent, + canActivate: [LoginGuard], children: [ { path: '', loadChildren: () => import('src/app/layouts/auth-layout/auth-layout.module').then(m => m.AuthLayoutModule) - } + }, ] - }, { + }, + + { path: '**', - redirectTo: 'dashboard' + redirectTo: '/dashboard' } ]; @@ -40,7 +86,7 @@ const routes: Routes =[ CommonModule, BrowserModule, RouterModule.forRoot(routes,{ - useHash: true + useHash: false }) ], exports: [ diff --git a/src/app/components/cards/display-product/display-product.component.html b/src/app/components/cards/display-product/display-product.component.html new file mode 100644 index 00000000..e30391a7 --- /dev/null +++ b/src/app/components/cards/display-product/display-product.component.html @@ -0,0 +1,12 @@ + +
+
+
+ Product image +
+
+

{{ product.name }}

+

{{ product.price }}

+
+
+
diff --git a/src/app/components/cards/display-product/display-product.component.scss b/src/app/components/cards/display-product/display-product.component.scss new file mode 100644 index 00000000..de6b8f12 --- /dev/null +++ b/src/app/components/cards/display-product/display-product.component.scss @@ -0,0 +1,10 @@ +.card-style{ + height: 30rem; + } + + .img-container{ + display: flex; + justify-content: center; + border-color: aqua; + border: 1px; + } diff --git a/src/app/components/cards/display-product/display-product.component.spec.ts b/src/app/components/cards/display-product/display-product.component.spec.ts new file mode 100644 index 00000000..5c039c02 --- /dev/null +++ b/src/app/components/cards/display-product/display-product.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DisplayProductComponent } from './display-product.component'; + +describe('DisplayProductComponent', () => { + let component: DisplayProductComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DisplayProductComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DisplayProductComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/cards/display-product/display-product.component.ts b/src/app/components/cards/display-product/display-product.component.ts new file mode 100644 index 00000000..27262ce1 --- /dev/null +++ b/src/app/components/cards/display-product/display-product.component.ts @@ -0,0 +1,23 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +@Component({ + selector: 'app-display-product', + templateUrl: './display-product.component.html', + styleUrl: './display-product.component.scss' +}) +export class DisplayProductComponent implements OnInit{ + + + + @Input() product: any; + @Output() select = new EventEmitter(); + + onSelect() { + this.select.emit(this.product); + } + + ngOnInit(): void { + console.log(this.product) + } + +} diff --git a/src/app/components/cards/item/item.component.html b/src/app/components/cards/item/item.component.html new file mode 100644 index 00000000..468a2495 --- /dev/null +++ b/src/app/components/cards/item/item.component.html @@ -0,0 +1,85 @@ + +
+
+
+ Product image +
+ +
+
+
{{ product.name }}
+
+ +
+
+ {{ variant.name }} +
+
+ +
+

{{selectedVariant.price}}

+
+ + + +
+ + + {{ size.size }} + + +
+ + +
+

Out of stock

+

Available: {{ selectedSetSize.quantity }}

+
+ + +
+ + + +
+ + + +
+

Out of stock

+

Available: {{ selectedVariant.quantity }}

+
+ + +
+ + + +
+ +
+ + +
+ + + + + + + +
+
+
diff --git a/src/app/components/cards/item/item.component.scss b/src/app/components/cards/item/item.component.scss new file mode 100644 index 00000000..6284a281 --- /dev/null +++ b/src/app/components/cards/item/item.component.scss @@ -0,0 +1,148 @@ +/* Style for the variant squares */ +.variant-square { + width: 100px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #007bff; /* Border color */ + margin: 5px; + cursor: pointer; + font-size: 14px; + text-align: center; + background-color: #f8f9fa; /* Background color */ + transition: background-color 0.3s, border-color 0.3s; + border-radius: 5px; + color: black; + } + + .variant-square:hover { + background-color: #e9ecef; /* Hover background color */ + } + + .variant-square.selected { + background-color: #007bff; /* Selected background color */ + color: white; + border-color: #0056b3; /* Selected border color */ + border-radius: 5px; + } + + /* Existing size square styles */ + .size-square { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #007bff; /* Border color */ + margin: 5px; + cursor: pointer; + font-size: 14px; + text-align: center; + background-color: #f8f9fa; /* Background color */ + transition: background-color 0.3s, border-color 0.3s; + } + + + .size-square.selected { + background-color: #007bff; /* Selected background color */ + color: white; + border-color: #0056b3; /* Selected border color */ + } + + .size-square.out-of-stock { + background-color: #d6d6d6; /* Out of stock background color */ + cursor: not-allowed; + color: #6c757d; + border-color: #6c757d; /* Out of stock border color */ + } + + .quantity-container { + display: flex; + align-items: center; + justify-content: center; + margin: 10px; + } + + .quantity-input { + width: 60px; + text-align: center; + margin: 0 10px; + } + + .quantity-button { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + background-color: aquamarine; + } + + .quantity-button:disabled{ + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + background-color: gray; + } + + .buy-buttons{ + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + gap: 2px; + + #addToCart{ + background-color: aqua; + border: aqua; + border-radius: 5px; + display: inline-block; + white-space: nowrap; /* Prevent text from wrapping */ + overflow: hidden; + font-size: 12px; + text-align: center; + padding: 10px; + } + #purchase{ + background-color:#007bff; + border: #007bff; + margin: 0.5rem; + border-radius: 5px; + display: inline-block; + font-size: 12px; + } + } + + .card-style{ + display: flex; + width: 100%; + border: 1px solid #ddd; + border-radius: 5px; + overflow: hidden; + } + + + .card-img-top { + width: 100%; + height: auto; + object-fit: cover; + } + + .card-title{ + font-size: x-large; + } + + .box{ + box-sizing: border-box; + border: 3px solid transparent; + background-clip:padding-box; + } + + .badge-info.active{ + color: #fff !important; + background-color: #0da5c0 !important; + } + \ No newline at end of file diff --git a/src/app/components/cards/item/item.component.spec.ts b/src/app/components/cards/item/item.component.spec.ts new file mode 100644 index 00000000..03c464ad --- /dev/null +++ b/src/app/components/cards/item/item.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ItemComponent } from './item.component'; + +describe('ItemComponent', () => { + let component: ItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ItemComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/cards/item/item.component.ts b/src/app/components/cards/item/item.component.ts new file mode 100644 index 00000000..216e73f0 --- /dev/null +++ b/src/app/components/cards/item/item.component.ts @@ -0,0 +1,235 @@ +import { ProductsService } from 'src/app/services/products.service'; +import { Inventory, Variant, Size } from './../../../models/inventory.model'; +import { AfterViewInit, Component, Input, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Product } from 'src/app/models/product.model'; +import { CartItem } from 'src/app/models/shoppingcart.model'; +import { AuthService } from 'src/app/services/auth.service'; +import { InventoryService } from 'src/app/services/inventory.service'; +import { ShoppingCartService } from 'src/app/services/shoppingcart.service'; +import { ToastService } from '../../modal/toast/toast.service'; +import { ToastComponent } from '../../modal/toast/toast.component'; + +@Component({ + selector: 'app-item', + templateUrl: './item.component.html', + styleUrls: ['./item.component.scss'] +}) +export class ItemComponent implements OnInit, AfterViewInit { + product: Product; + inventory: Inventory; + variants: Variant[] = []; + sizesForSet: Size[] = []; + selectedVariant: Variant | null = null; + selectedSetSize: Size | null = null; + maxQuantity = 0; + quantity = 1; + selectedItem: CartItem[] = []; + @ViewChild(ToastComponent) toastComponent!: ToastComponent; + + constructor( + private inventoryService: InventoryService, + private route: ActivatedRoute, + private productService: ProductsService, + private shoppingCartService: ShoppingCartService, + private authService: AuthService, + private router: Router, + private toastService: ToastService, + ) {} + + ngOnInit(): void { + this.getProductCodeFromRoute(); + } + + ngAfterViewInit() { + this.toastService.registerToast(this.toastComponent); + } + + getProductCodeFromRoute(): void { + this.route.params.subscribe(params => { + const code = params['code']; + if (code) { + this.getProductByCode(code) + this.getInventoryItemByCode(code); // Fetch the product by code + } + }); + } + + getProductByCode(code: string): void { + this.productService.getProductByCode(code).subscribe(data => { + this.product = data; + }); + } + + getInventoryItemByCode(code: string){ + this.inventoryService.getInventoryByProductCode(code).subscribe(data => { + if (data) { + this.inventory = data; + console.log(this.inventory) + this.getInventoryItems(); + console.log(this.inventory) + if (this.variants.length > 0) { + this.selectVariant(this.variants[0]) + if (this.selectedVariant.sizes && this.selectedVariant.sizes.length > 0) { + this.selectSetSize(this.selectedVariant.sizes[0]); + } + } + } else { + console.warn('No inventory found.'); + } + }); + } + + getInventoryItems() { + if (this.inventory.variants) { + this.variants = [...this.inventory.variants]; + + // Check if any variant has sizes + const hasSizes = this.variants.some(variant => variant.sizes && variant.sizes.length > 0); + + if (this.inventory.isSet && hasSizes) { + this.createSizesForSet(); + this.variants.push({ + code: 'SET', + name: 'Set' , + price: this.product.price, + sizes: this.sizesForSet + } as Variant); + } else if (this.inventory.isSet && !hasSizes) { + const minQuantity = Math.min(...this.variants.map(variant => variant.quantity || Infinity)); + + this.variants.push({ + code: 'SET', + name: 'Set', + price: this.product.price, + quantity: minQuantity === Infinity ? 0 : minQuantity + } as Variant); + } + } + } + + + createSizesForSet() { + if (this.inventory.isSet && this.inventory.variants) { + const sizeMap: { [sizeName: string]: number } = {}; + + this.inventory.variants.forEach(variant => { + variant.sizes?.forEach(size => { + const sizeName = size.size; + const quantity = size.quantity === undefined || size.quantity === null ? 0 : size.quantity; + + if (sizeMap[sizeName] === undefined) { + sizeMap[sizeName] = quantity; + } else { + sizeMap[sizeName] = Math.min(sizeMap[sizeName], quantity); + } + }); + }); + + this.sizesForSet = Object.keys(sizeMap).map(sizeName => ({ + size: sizeName, + quantity: sizeMap[sizeName] + })) as Size[]; + } + + } + + selectSetSize(size: any): void { + this.selectedSetSize = size; + this.maxQuantity = size.quantity; + if(this.maxQuantity==0){ + this.quantity = 0 + }else{ + this.quantity = 1; + } + } + + selectVariant(variant: Variant) { + this.selectedVariant = variant; + console.log("selectedVariant", this.selectedVariant) + if(!this.selectedSetSize){ + this.maxQuantity = variant.quantity; + if(this.maxQuantity==0){ + this.quantity = 0 + }else{ + this.quantity = 1; + } + }else{ + this.selectSetSize(this.selectedVariant.sizes[0]) + } + } + + increaseQuantity(): void { + if (this.quantity < this.maxQuantity) { + this.quantity++; + } + } + + decreaseQuantity(): void { + if (this.quantity > 1) { + this.quantity--; + } + } + + addToCart(): void { + if (this.selectedVariant && this.maxQuantity>0) { + const cartItem: CartItem = { + cartID: this.generateUniqueCartID(), + idNo: this.authService.getUserIdNo(), // Replace with actual user ID + orderDate: new Date().toISOString(), // Current date + productCode: this.product.code, + variantCode: this.selectedVariant.code, + price: this.selectedVariant.price, + quantity: this.quantity, + totalPrice: this.selectedVariant.price * this.quantity, + imgURL: this.selectedVariant.imgURL || '', + size: this.selectedSetSize ? this.selectedSetSize.size : '' , + name: this.selectedVariant.name === "Set" ? "Set - " + this.product.name : this.selectedVariant.name, + productName: this.product.name + }; + + this.shoppingCartService.addToCart(cartItem); + const message = cartItem.size + ? `${cartItem.name} size ${cartItem.size} successfully added to cart!` + : `${cartItem.name} successfully added to cart!`; + this.toastService.showToast(message, 'success'); + + } else { + const message = this.selectedSetSize + ? `${this.selectedVariant.name} size ${this.selectedSetSize.size} is not available!` + : `${this.selectedVariant.name} is not available!`; + this.toastService.showToast(message, 'error'); + } + } + + generateUniqueCartID(): number { + return Date.now() + Math.floor(Math.random() * 1000); + } + + proceedToCheckOut(){ + if (this.product && this.selectedVariant && this.maxQuantity>0) { + const cartItem: CartItem = { + // cartID: this.generateUniqueCartID(), + idNo: this.authService.getUserIdNo(), // Replace with actual user ID + orderDate: new Date().toISOString(), // Current date + productCode: this.product.code, + variantCode: this.selectedVariant.code, + price: this.selectedVariant.price, + quantity: this.quantity, + totalPrice: this.selectedVariant.price * this.quantity, + imgURL: this.selectedVariant.imgURL || '', + size: this.selectedSetSize ? this.selectedSetSize.size : '' , + name: this.selectedVariant.name === "Set" ? "Set - " + this.product.name : this.selectedVariant.name, + productName: this.product.name + }; + this.selectedItem.push(cartItem) + sessionStorage.setItem('selectedItems', JSON.stringify(this.selectedItem)); + + this.router.navigate([`/student/products/${this.product.code}/checkout`]); + }else{ + this.toastService.showToast("Selected product is not available at the moment", 'error'); + } + } + +} + diff --git a/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.html b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.html new file mode 100644 index 00000000..88937546 --- /dev/null +++ b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.html @@ -0,0 +1,37 @@ +
+
+
+ +
+
+ Product image +
+
+

{{cartItem.name}}

+
+
+

{{cartItem.price}}

+
+
+

{{cartItem.size}}

+
+
+
+ + + +
+
+
+

{{cartItem.totalPrice}}

+
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.scss b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.scss new file mode 100644 index 00000000..138536ce --- /dev/null +++ b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.scss @@ -0,0 +1,4 @@ +.border-select { + border: 4px solid !important; + border-color: #0d93aa !important; +} \ No newline at end of file diff --git a/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.spec.ts b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.spec.ts new file mode 100644 index 00000000..9d168607 --- /dev/null +++ b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShoppingcartItemComponent } from './shoppingcart-item.component'; + +describe('ShoppingcartItemComponent', () => { + let component: ShoppingcartItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ShoppingcartItemComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ShoppingcartItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.ts b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.ts new file mode 100644 index 00000000..ea843b6f --- /dev/null +++ b/src/app/components/cards/shoppingcart-item/shoppingcart-item.component.ts @@ -0,0 +1,55 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { CartItem } from 'src/app/models/shoppingcart.model'; + +@Component({ + selector: 'app-shoppingcart-item', + templateUrl: './shoppingcart-item.component.html', + styleUrl: './shoppingcart-item.component.scss' +}) + +export class ShoppingcartItemComponent implements OnInit { + @Input() cartItem: CartItem; + @Output() selectionChange = new EventEmitter(); + @Output() remove = new EventEmitter(); + + ngOnInit(): void { + } + + + onCheckboxChange(): void { + this.selectionChange.emit(this.cartItem); + } + + decreaseQuantity(): void { + if (this.cartItem.quantity > 1) { + this.cartItem.quantity--; + this.updateTotalPrice(); + } + } + + + increaseQuantity(): void { + if (this.cartItem.quantity < this.cartItem.maxQuantity) { + this.cartItem.quantity++; + this.updateTotalPrice(); + } + } + + updateTotalPrice(): void { + this.cartItem.totalPrice = this.cartItem.quantity * this.cartItem.price; + } + + removeItem(): void { + this.remove.emit(this.cartItem); + } + + toggleCheckbox(event: MouseEvent): void { + // Prevent toggling if clicking directly on the checkbox + const target = event.target as HTMLElement; + if (target.tagName !== 'INPUT') { + this.cartItem.selected = !this.cartItem.selected; + } + this.selectionChange.emit(this.cartItem); + } + +} diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index b1add21a..80695de5 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -5,22 +5,42 @@ import { NavbarComponent } from './navbar/navbar.component'; import { FooterComponent } from './footer/footer.component'; import { RouterModule } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ItemComponent } from './cards/item/item.component'; +import { TableComponent } from './table/table.component'; +import { DisplayProductComponent } from './cards/display-product/display-product.component'; +import { ShoppingcartItemComponent } from './cards/shoppingcart-item/shoppingcart-item.component'; +import { FormsModule } from '@angular/forms'; +import { PromptDialogComponent } from './modal/prompt-dialog/prompt-dialog.component'; +import { ToastComponent } from './modal/toast/toast.component'; @NgModule({ imports: [ CommonModule, RouterModule, - NgbModule + NgbModule, + FormsModule, ], declarations: [ FooterComponent, NavbarComponent, - SidebarComponent + SidebarComponent, + ItemComponent, + DisplayProductComponent, + TableComponent, + ShoppingcartItemComponent, + PromptDialogComponent, + ToastComponent ], exports: [ FooterComponent, NavbarComponent, - SidebarComponent + SidebarComponent, + ItemComponent, + DisplayProductComponent, + TableComponent, + ShoppingcartItemComponent, + PromptDialogComponent, + ToastComponent ] }) export class ComponentsModule { } diff --git a/src/app/components/modal/prompt-dialog/prompt-dialog.component.html b/src/app/components/modal/prompt-dialog/prompt-dialog.component.html new file mode 100644 index 00000000..9a43c337 --- /dev/null +++ b/src/app/components/modal/prompt-dialog/prompt-dialog.component.html @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/src/app/components/modal/prompt-dialog/prompt-dialog.component.scss b/src/app/components/modal/prompt-dialog/prompt-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/modal/prompt-dialog/prompt-dialog.component.spec.ts b/src/app/components/modal/prompt-dialog/prompt-dialog.component.spec.ts new file mode 100644 index 00000000..69066ae7 --- /dev/null +++ b/src/app/components/modal/prompt-dialog/prompt-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PromptDialogComponent } from './prompt-dialog.component'; + +describe('PromptDialogComponent', () => { + let component: PromptDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PromptDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PromptDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/modal/prompt-dialog/prompt-dialog.component.ts b/src/app/components/modal/prompt-dialog/prompt-dialog.component.ts new file mode 100644 index 00000000..67c4b036 --- /dev/null +++ b/src/app/components/modal/prompt-dialog/prompt-dialog.component.ts @@ -0,0 +1,66 @@ +import { Component, ElementRef, EventEmitter, Input, Output, Renderer2, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-prompt-dialog', + templateUrl: './prompt-dialog.component.html', + styleUrl: './prompt-dialog.component.scss' +}) +export class PromptDialogComponent { + + @ViewChild('modal') modalElement!: ElementRef; + @Input() title: string = ''; + @Input() message: string = ''; + @Input() confirmButtonLabel: string = ''; + @Input() cancelButtonLabel: string = ''; + @Output() confirmActionButtonClick = new EventEmitter(); + @Output() cancelActionButtonClick = new EventEmitter(); + @Input() buttonColorClass: string = "info" + private backdropElement: HTMLElement; + + constructor(private renderer: Renderer2) { + // Create the backdrop element and configure its styles + this.backdropElement = this.renderer.createElement('div'); + this.renderer.addClass(this.backdropElement, 'modal-backdrop'); + this.renderer.addClass(this.backdropElement, 'fade'); + } + + open() { + // Show the modal with fade effect + this.renderer.addClass(this.modalElement.nativeElement, 'show'); + this.modalElement.nativeElement.style.display = 'block'; + document.body.classList.add('modal-open'); + + // Append the backdrop element to the body with fade in + this.renderer.appendChild(document.body, this.backdropElement); + setTimeout(() => { + this.renderer.addClass(this.backdropElement, 'show'); + }, 10); // Small delay to trigger CSS transition + } + + close() { + // Remove the modal's show class and hide it + this.renderer.removeClass(this.modalElement.nativeElement, 'show'); + setTimeout(() => { + this.modalElement.nativeElement.style.display = 'none'; + }, 150); // Delay to match CSS fade out duration + + // Remove the modal-open class from the body + document.body.classList.remove('modal-open'); + + // Fade out and remove the backdrop + this.renderer.removeClass(this.backdropElement, 'show'); + setTimeout(() => { + this.renderer.removeChild(document.body, this.backdropElement); + }, 150); // Same delay as modal fade out + } + + onComfirmButtonActionClick(): void { + this.confirmActionButtonClick.emit(); + this.close(); + } + + onCancelButtonActionClick(): void { + this.cancelActionButtonClick.emit(); + this.close(); + } +} diff --git a/src/app/components/modal/toast/toast.component.html b/src/app/components/modal/toast/toast.component.html new file mode 100644 index 00000000..88777187 --- /dev/null +++ b/src/app/components/modal/toast/toast.component.html @@ -0,0 +1,7 @@ +
+ {{ message }} +
\ No newline at end of file diff --git a/src/app/components/modal/toast/toast.component.scss b/src/app/components/modal/toast/toast.component.scss new file mode 100644 index 00000000..5b744508 --- /dev/null +++ b/src/app/components/modal/toast/toast.component.scss @@ -0,0 +1,9 @@ +:host { + position: fixed; /* Ensures the toast is fixed to the viewport */ + top: 100px; /* Distance from the top of the viewport */ + left: 50%; /* Center horizontally */ + transform: translateX(-50%); /* Offset by half its width to center it */ + z-index: 1050; /* Bootstrap default for modals */ + pointer-events: none; + width: 30%; /* Disable pointer events on the toast host */ +} \ No newline at end of file diff --git a/src/app/components/modal/toast/toast.component.spec.ts b/src/app/components/modal/toast/toast.component.spec.ts new file mode 100644 index 00000000..f9c33e87 --- /dev/null +++ b/src/app/components/modal/toast/toast.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToastComponent } from './toast.component'; + +describe('ToastComponent', () => { + let component: ToastComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToastComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToastComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/modal/toast/toast.component.ts b/src/app/components/modal/toast/toast.component.ts new file mode 100644 index 00000000..fe68a917 --- /dev/null +++ b/src/app/components/modal/toast/toast.component.ts @@ -0,0 +1,29 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-toast', + templateUrl: './toast.component.html', + styleUrl: './toast.component.scss' +}) +export class ToastComponent { + @Input() message: string = '' + @Input() show: boolean = false; + @Input() type: 'success' | 'error' | 'info' = 'success'; // You can extend this as needed + timeout: any; + + display(message: string, type: 'success' | 'error' | 'info', duration: number = 3000): void { + this.message = message; + this.type = type; + this.show = true; + + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + this.hide(); + }, duration); + } + + hide(): void { + this.show = false; + } + +} diff --git a/src/app/components/modal/toast/toast.service.spec.ts b/src/app/components/modal/toast/toast.service.spec.ts new file mode 100644 index 00000000..e0413db8 --- /dev/null +++ b/src/app/components/modal/toast/toast.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ToastService } from './toast.service'; + +describe('ToastService', () => { + let service: ToastService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ToastService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/components/modal/toast/toast.service.ts b/src/app/components/modal/toast/toast.service.ts new file mode 100644 index 00000000..97a31372 --- /dev/null +++ b/src/app/components/modal/toast/toast.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { ToastComponent } from './toast.component'; + +@Injectable({ + providedIn: 'root' +}) +export class ToastService { + + constructor() { } + + private toastComponent: ToastComponent | undefined; + + public registerToast(toast: ToastComponent): void { + this.toastComponent = toast; + } + + public showToast(message: string, type: 'success' | 'error' | 'info', duration: number = 3000): void { + if (this.toastComponent) { + this.toastComponent.display(message, type, duration); + } + } +} diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html index 533724cf..3919f91e 100644 --- a/src/app/components/navbar/navbar.component.html +++ b/src/app/components/navbar/navbar.component.html @@ -19,11 +19,12 @@