diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e36d0ad..642c257 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,7 +1,7 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; - +import { provideHttpClient } from '@angular/common/http'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { @@ -9,6 +9,7 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimations(), + provideHttpClient(), ] }; diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..180b92c --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,9 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app.component'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +bootstrapApplication(AppComponent, { + providers: [ + provideHttpClient(withInterceptorsFromDi()) + ] +}); \ No newline at end of file diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 7308758..f1d42b7 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -15,6 +15,15 @@ import { authGuard } from './gaurds/auth.guard' import { roleGuard } from './gaurds/role.guard'; import { VendorSignupComponent } from './components/auth-page/vendor-signup/vendor-signup.component'; import { SupplierSignupComponent } from './components/auth-page/supplier-signup/supplier-signup.component'; +import { VendorComponent } from './components/vendor-page/vender/vendor.component'; +import { ProductSectionComponent } from './components/vendor-page/product-section/product-section.component'; +import { CartComponent } from './components/vendor-page/cart/cart.component'; +import { OrderSummaryComponent } from './components/vendor-page/order-summary/order-summary.component'; +import { CartSummaryComponent } from './components/vendor-page/cart-summary/cart-summary.component'; +import { OrdersComponent } from './components/vendor-page/orders/orders.component'; + +import { WarehouseComponent } from './components/admin-page/warehouse/warehouse.component'; + export const routes: Routes = [ { @@ -35,7 +44,7 @@ export const routes: Routes = [ { path: 'dashboard', component: DashboardLayoutComponent, - canActivate: [authGuard], + // canActivate: [authGuard], children: [ { path: '', component: ProfileComponent, pathMatch: 'full' }, // /dashboard { path: 'profile', component: ProfileComponent, pathMatch: 'full' }, @@ -44,6 +53,7 @@ export const routes: Routes = [ component: ForecastComponent, pathMatch: 'full', }, + { path: 'warehouse', component: WarehouseComponent, pathMatch: 'full' }, { path: 'order-history', component: OrderHistoryComponent }, // /dashboard/orders { path: 'inventory', component: InventoryComponent }, // /dashboard/inventory { path: 'product-management', component: ProductManagementComponent}, // /dashboard/product-management @@ -51,6 +61,12 @@ export const routes: Routes = [ // { path: 'deliveries', component: DeliveriesComponent }, // /dashboard/deliveries // { path: 'vendors', component: VendorsComponent }, { path: 'supplier', component: SupplierDashboard, pathMatch: 'full' }, + { path: 'vendor', component: VendorComponent, pathMatch: 'full' }, + { path: 'vendor/products', component: ProductSectionComponent, pathMatch: 'full' }, + { path: 'vendor/cart', component: CartComponent, pathMatch: 'full' }, + { path: 'vendor/orders/order-details/:id', component: OrderSummaryComponent, pathMatch: 'full' }, + { path: 'vendor/cart-summery', component: CartSummaryComponent, pathMatch: 'full' }, + { path: 'vendor/orders', component: OrdersComponent, pathMatch: 'full' }, ] }, { path: '**', redirectTo: 'home' }, // Handle 404/unknown routes diff --git a/src/app/components/admin-page/warehouse/warehouse.component.css b/src/app/components/admin-page/warehouse/warehouse.component.css new file mode 100644 index 0000000..1daef03 --- /dev/null +++ b/src/app/components/admin-page/warehouse/warehouse.component.css @@ -0,0 +1,189 @@ +/* Warehouse Card Styles */ +.warehouse-card { + transition: transform 0.3s ease, box-shadow 0.3s ease; + } + + .warehouse-card:hover { + transform: translateY(-3px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); + } + + /* Gradient Backgrounds */ + .warehouse-header-gradient { + background-image: linear-gradient(135deg, #4f46e5 0%, #3b82f6 100%); + } + + .inventory-header-gradient { + background-image: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + } + + /* Custom Badge Styles */ + .custom-badge { + border-radius: 9999px; + padding: 0.25rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + display: inline-flex; + align-items: center; + } + + .badge-blue { + background-color: rgba(59, 130, 246, 0.1); + color: #1e40af; + } + + .badge-green { + background-color: rgba(16, 185, 129, 0.1); + color: #065f46; + } + + .badge-amber { + background-color: rgba(245, 158, 11, 0.1); + color: #92400e; + } + + /* Status Indicators */ + .status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 6px; + } + + .status-active { + background-color: #10b981; + } + + .status-inactive { + background-color: #ef4444; + } + + /* Button Animations */ + .btn-primary { + transition: all 0.2s ease; + position: relative; + overflow: hidden; + } + + .btn-primary:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 5px; + height: 5px; + background: rgba(255, 255, 255, 0.5); + opacity: 0; + border-radius: 100%; + transform: scale(1, 1) translate(-50%); + transform-origin: 50% 50%; + } + + .btn-primary:focus:not(:active)::after { + animation: ripple 1s ease-out; + } + + @keyframes ripple { + 0% { + transform: scale(0, 0); + opacity: 0.5; + } + 20% { + transform: scale(25, 25); + opacity: 0.3; + } + 100% { + opacity: 0; + transform: scale(40, 40); + } + } + + /* Search Input Focus Effect */ + .search-input:focus { + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); + } + + /* Table Row Hover Effect */ + .inventory-row { + transition: background-color 0.2s ease; + } + + .inventory-row:hover { + background-color: rgba(243, 244, 246, 0.8); + } + + /* Loading Animation */ + .loading-spinner { + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + /* Back Button Hover Effect */ + .back-button { + transition: transform 0.2s ease; + } + + .back-button:hover { + transform: translateX(-3px); + } + + /* Responsive Adjustments */ + @media (max-width: 768px) { + .warehouse-grid { + grid-template-columns: 1fr; + } + + .stats-container { + flex-direction: column; + } + + .stats-item { + margin-bottom: 1rem; + } + } + + /* Inventory Quantity Tags */ + .quantity-high { + background-color: #d1fae5; + color: #065f46; + } + + .quantity-medium { + background-color: #fef3c7; + color: #92400e; + } + + .quantity-low { + background-color: #fee2e2; + color: #b91c1c; + } + + /* Page Transitions */ + .page-enter { + opacity: 0; + transform: translateY(10px); + } + + .page-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity 300ms, transform 300ms; + } + + .page-exit { + opacity: 1; + } + + .page-exit-active { + opacity: 0; + transition: opacity 300ms; + } \ No newline at end of file diff --git a/src/app/components/admin-page/warehouse/warehouse.component.html b/src/app/components/admin-page/warehouse/warehouse.component.html new file mode 100644 index 0000000..d906565 --- /dev/null +++ b/src/app/components/admin-page/warehouse/warehouse.component.html @@ -0,0 +1,280 @@ +
+ +
+
+
+ + + +
+
+ + +
+
+
+ Warehouse Management +
+

+ {{ selectedWarehouse ? 'Inventory for' : 'All Warehouses' }} +

+

+ {{ selectedWarehouse ? selectedWarehouse.warehouse_name : 'Warehouse Directory' }} +

+

+ {{ selectedWarehouse ? 'View and manage inventory' : 'Manage your storage facilities' }} +

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

Warehouses

+ + {{ warehouses.length }} locations + +
+ +
+ +
+ +
+ + + +
+
+
+
+ + +
+
+
+
+

{{ warehouse.warehouse_name }}

+ ID: {{ warehouse.id }} +
+
+
+ + + + + {{ warehouse.location_x }}, {{ warehouse.location_y }} +
+
+ + + + Capacity: {{ parseFloat(warehouse.capacity).toLocaleString() }} units +
+
+ + + + Created: {{ formatDate(warehouse.created_at) }} +
+ +
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+
+ + +
+
+
+
+ + + +

{{ selectedWarehouse.warehouse_name }}

+ ID: {{ selectedWarehouse.id }} +
+

{{ selectedWarehouse.location_x }}, {{ selectedWarehouse.location_y }}

+
+
+
+
Warehouse Capacity
+
{{ parseFloat(selectedWarehouse.capacity).toLocaleString() }} units
+
+
+
+
+ + +
+
+
+ + + + +
+
+ +
+
+
+

{{ item.product_name }}

+

Category: {{ item.category }}

+
+ + {{ item.product_count.toLocaleString() }} units + +
+

Supplied: {{ formatDate(item.supplied_date) }} by {{ item.supplied_by }}

+
+ + + +
+
+
+
+
+
+ + + +
+
+ Loading... +
+
+ + + +
+
+ + + +
+

No warehouses found

+

+ {{ warehouses.length > 0 ? 'No warehouses match your search.' : 'There are no warehouses in the system yet.' }} +

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

No inventory items found

+

+ {{ inventoryProducts.length > 0 ? 'No inventory items match your search.' : 'This warehouse is currently empty.' }} +

+
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/components/admin-page/warehouse/warehouse.component.spec.ts b/src/app/components/admin-page/warehouse/warehouse.component.spec.ts new file mode 100644 index 0000000..65e7fda --- /dev/null +++ b/src/app/components/admin-page/warehouse/warehouse.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +import { WarehouseComponent } from './warehouse.component'; + +describe('WarehouseComponent', () => { + let component: WarehouseComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WarehouseComponent, HttpClientTestingModule] + }) + .compileComponents(); + + fixture = TestBed.createComponent(WarehouseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/app/components/admin-page/warehouse/warehouse.component.ts b/src/app/components/admin-page/warehouse/warehouse.component.ts new file mode 100644 index 0000000..844d000 --- /dev/null +++ b/src/app/components/admin-page/warehouse/warehouse.component.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { WarehouseService, Warehouse, WarehouseInventory, InventoryProductDetail } from '../../../services/warehouse.service'; + +@Component({ + selector: 'app-warehouse', + standalone: true, + imports: [CommonModule, FormsModule, RouterModule], + templateUrl: './warehouse.component.html', + styleUrls: ['./warehouse.component.css'] +}) +export class WarehouseComponent implements OnInit { + warehouses: Warehouse[] = []; + inventoryData: WarehouseInventory | null = null; + inventoryProducts: InventoryProductDetail[] = []; + selectedWarehouse: Warehouse | null = null; + loading = false; + searchQuery = ''; + filteredWarehouses: Warehouse[] = []; + + // For inventory view + inventoryLoading = false; + inventorySearchQuery = ''; + filteredInventoryProducts: InventoryProductDetail[] = []; + + constructor(private warehouseService: WarehouseService) { } + + ngOnInit(): void { + this.loadWarehouses(); + } + + // Add this method to make parseFloat available in the template + parseFloat(value: string): number { + return parseFloat(value); + } + + loadWarehouses(): void { + this.loading = true; + this.warehouseService.getWarehouses().subscribe({ + next: (data) => { + this.warehouses = data; + this.filteredWarehouses = [...this.warehouses]; + this.loading = false; + }, + error: (error) => { + console.error('Error loading warehouses', error); + this.loading = false; + } + }); + } + + selectWarehouse(warehouse: Warehouse): void { + this.selectedWarehouse = warehouse; + this.loadInventory(warehouse.id); + } + + loadInventory(warehouseId: number): void { + this.inventoryLoading = true; + this.warehouseService.getWarehouseInventory(warehouseId).subscribe({ + next: (data) => { + this.inventoryData = data; + this.inventoryProducts = data.inventory_product_details; + this.filteredInventoryProducts = [...this.inventoryProducts]; + this.inventoryLoading = false; + }, + error: (error) => { + console.error('Error loading inventory', error); + this.inventoryLoading = false; + } + }); + } + + backToWarehouses(): void { + this.selectedWarehouse = null; + this.inventoryData = null; + this.inventoryProducts = []; + } + + filterWarehouses(): void { + if (!this.searchQuery.trim()) { + this.filteredWarehouses = [...this.warehouses]; + return; + } + + const query = this.searchQuery.toLowerCase(); + this.filteredWarehouses = this.warehouses.filter(warehouse => + warehouse.warehouse_name.toLowerCase().includes(query) || + warehouse.location_x.toLowerCase().includes(query) || + warehouse.location_y.toLowerCase().includes(query) + ); + } + + filterInventory(): void { + if (!this.inventorySearchQuery.trim()) { + this.filteredInventoryProducts = [...this.inventoryProducts]; + return; + } + + const query = this.inventorySearchQuery.toLowerCase(); + this.filteredInventoryProducts = this.inventoryProducts.filter(item => + item.product_name.toLowerCase().includes(query) || + item.category.toLowerCase().includes(query) || + item.supplied_by.toLowerCase().includes(query) + ); + } + + formatDate(dateString: string): string { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + } + + formatNumber(value: number): string { + return value.toLocaleString(); + } +} \ No newline at end of file diff --git a/src/app/components/home-page/product-section/product-section.component.html b/src/app/components/home-page/product-section/product-section.component.html index b68850d..59291f6 100644 --- a/src/app/components/home-page/product-section/product-section.component.html +++ b/src/app/components/home-page/product-section/product-section.component.html @@ -102,6 +102,7 @@

@@ -110,7 +111,7 @@

+
+ First name is required +
+
+
+ + +
+ Last name is required +
+
+

+ +
+ + +
+ Address is required +
+
+ +
+
+ + +
+ City is required +
+
+
+ + +
+ Country is required +
+
+
+ + +
+ Postal code is required +
+
+
+ +
+ + +
+ Valid phone number is required +
+
+ + + + + +
+
+

+ Order Summary +

+ +
+
+

+ Subtotal ({{ getTotalItems() }} items): + ${{ getSubtotal() }} +

+

+ Shipping: + ${{ shippingCost.toFixed(2) }} +

+

+ Tax ({{ taxRate }}%): + ${{ calculateTax().toFixed(2) }} +

+
+ + +
+
+ + +
+
+ Discount applied: -${{ discountAmount.toFixed(2) }} +
+
+ + +
+ Total: + ${{ getTotal() }} +
+ + + + + + Return to Cart + +
+
+
+ + + + +
+
+ + + +
+

Your cart is currently empty.

+ Browse Products +
+
+ + + +
+ +
+ +
+

Payment Method

+ + +
+
+
+ {{ method.name }} + {{ method.name }} +
+
+
+ + +
+
+
+ + +
+ Cardholder name is required +
+
+ +
+ +
+ +
+
+ Visa + Mastercard + Amex +
+
+
+
+ Enter a valid card number +
+
+ +
+
+ + +
+ Enter a valid expiry date +
+
+
+ + +
+ Enter a valid CVV +
+
+
+ +
+ + +
+
+
+ + +
+ PayPal +

After clicking "Complete Order", you will be redirected to PayPal to complete your purchase securely.

+
+

Total: ${{ getTotal() }}

+
+
+ + +
+ +

After clicking "Complete Order", you'll be prompted to confirm payment.

+
+

Total: ${{ getTotal() }}

+
+
+ + +
+
+

Billing Address

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

+ Order Summary +

+ +
+
+
+
+ {{ item.name }} +
+

{{ item.name }}

+

Qty: {{ item.quantity }}

+
+
+

+ ${{ (item.price * item.quantity).toFixed(2) }} +

+
+
+ +
+

+ Subtotal: + ${{ getSubtotal() }} +

+

+ Shipping: + ${{ shippingCost.toFixed(2) }} +

+

+ Tax: + ${{ calculateTax().toFixed(2) }} +

+
+ Discount: + -${{ discountAmount.toFixed(2) }} +
+
+ +
+ Total: + ${{ getTotal() }} +
+ + + + +
+
+
+
+
+ + +
+
+
+ + + +
+ +

Order Confirmed!

+

Thank you for your purchase, {{ user.name }}. Your order has been received and is being processed.

+ +
+
+
+

ORDER NUMBER

+

{{ orderNumber }}

+
+
+

DATE

+

{{ orderDate | date:'mediumDate' }}

+
+
+

TOTAL

+

${{ getTotal() }}

+
+
+

PAYMENT METHOD

+

{{ getSelectedPaymentMethod()?.name }}

+
+
+
+ + +
+
+ + + + +
+
+ © 2025 Vendor Ordering System. All rights reserved. +
+
+ \ No newline at end of file diff --git a/src/app/components/vendor-page/cart-summary/cart-summary.component.spec.ts b/src/app/components/vendor-page/cart-summary/cart-summary.component.spec.ts new file mode 100644 index 0000000..3f495bb --- /dev/null +++ b/src/app/components/vendor-page/cart-summary/cart-summary.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CartSummaryComponent } from './cart-summary.component'; + +describe('CartSummaryComponent', () => { + let component: CartSummaryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CartSummaryComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CartSummaryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/cart-summary/cart-summary.component.ts b/src/app/components/vendor-page/cart-summary/cart-summary.component.ts new file mode 100644 index 0000000..7be7342 --- /dev/null +++ b/src/app/components/vendor-page/cart-summary/cart-summary.component.ts @@ -0,0 +1,283 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; + +interface CartItem { + id: number; + name: string; + description: string; + price: number; + quantity: number; + image: string; +} + +interface PaymentMethod { + id: string; + name: string; + icon: string; +} + +@Component({ + selector: 'app-cart-summary', + templateUrl: './cart-summary.component.html', + styleUrls: ['./cart-summary.component.css'], + standalone: true, + imports: [CommonModule, ReactiveFormsModule, FormsModule, RouterLink] +}) +export class CartSummaryComponent implements OnInit { + currentStep: 'summary' | 'payment' | 'confirmation' = 'summary'; + cartItems: CartItem[] = []; + shippingForm: FormGroup; + billingForm: FormGroup; + paymentForm: FormGroup; + + shippingCost: number = 5.99; + taxRate: number = 7; + discountCode: string = ''; + discountApplied: boolean = false; + discountAmount: number = 0; + + user = { name: 'Guest User' }; + sameAsShipping: boolean = true; + + paymentMethods: PaymentMethod[] = [ + { id: 'credit_card', name: 'Credit Card', icon: 'https://th.bing.com/th/id/R.d4653ffdddd3f73959889432f9d7d5f8?rik=0ZtmYR5u4PqXJQ&pid=ImgRaw&r=0' }, + { id: 'paypal', name: 'PayPal', icon: 'https://logodix.com/logo/370282.jpg' }, + { id: 'bank_transfer', name: 'Bank Transfer', icon: 'https://www.sevenjackpots.com/wp-content/uploads/2021/04/bank-transfer-logo.png' } + ]; + + selectedPaymentMethod: string = 'credit_card'; + orderNumber: string = ''; + orderDate: Date = new Date(); + + constructor( + private fb: FormBuilder, + private router: Router + ) { + this.shippingForm = this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + address: ['', Validators.required], + city: ['', Validators.required], + country: ['', Validators.required], + postalCode: ['', Validators.required], + phone: ['', [Validators.required, Validators.pattern('^[0-9]{10,15}$')]] + }); + + this.billingForm = this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + address: ['', Validators.required], + city: ['', Validators.required], + country: ['', Validators.required], + postalCode: ['', Validators.required] + }); + + this.paymentForm = this.fb.group({ + cardholderName: ['', Validators.required], + cardNumber: ['', [Validators.required, Validators.pattern('^[0-9]{16,19}$')]], + expiryDate: ['', [Validators.required, Validators.pattern('^(0[1-9]|1[0-2])\/?([0-9]{2})$')]], + cvv: ['', [Validators.required, Validators.pattern('^[0-9]{3,4}$')]], + saveCard: [false] + }); + } + + ngOnInit(): void { + this.loadCartItems(); + this.loadUserData(); + this.generateOrderNumber(); + } + + loadCartItems(): void { + // Load from localStorage or service + const savedCart = localStorage.getItem('cart'); + this.cartItems = savedCart ? JSON.parse(savedCart) : [ + { + id: 1, + name: 'Product 1', + description: 'High-quality product with premium features', + price: 49.99, + quantity: 2, + image: '/assets/images/product1.jpg' + }, + { + id: 2, + name: 'Product 2', + description: 'Versatile and durable for everyday use', + price: 29.99, + quantity: 1, + image: '/assets/images/product2.jpg' + } + ]; + } + + loadUserData(): void { + const savedUser = localStorage.getItem('user'); + if (savedUser) { + this.user = JSON.parse(savedUser); + + const userData = JSON.parse(savedUser); + if (userData.address) { + this.shippingForm.patchValue({ + firstName: userData.firstName || '', + lastName: userData.lastName || '', + address: userData.address.street || '', + city: userData.address.city || '', + country: userData.address.country || '', + postalCode: userData.address.postalCode || '', + phone: userData.phone || '' + }); + } + } + } + + generateOrderNumber(): void { + const randomNum = Math.floor(Math.random() * 90000) + 10000; + this.orderNumber = `ORD-${randomNum}`; + } + + increaseQuantity(index: number): void { + this.cartItems[index].quantity += 1; + this.updateCart(); + } + + decreaseQuantity(index: number): void { + if (this.cartItems[index].quantity > 1) { + this.cartItems[index].quantity -= 1; + this.updateCart(); + } + } + + removeItem(index: number): void { + this.cartItems.splice(index, 1); + this.updateCart(); + } + + updateCart(): void { + localStorage.setItem('cart', JSON.stringify(this.cartItems)); + } + + getSubtotal(): number { + return parseFloat((this.cartItems.reduce((acc, item) => acc + (item.price * item.quantity), 0)).toFixed(2)); + } + + calculateTax(): number { + return parseFloat((this.getSubtotal() * this.taxRate / 100).toFixed(2)); + } + + getTotalItems(): number { + return this.cartItems.reduce((acc, item) => acc + item.quantity, 0); + } + + getTotal(): number { + const subtotal = this.getSubtotal(); + const tax = this.calculateTax(); + return parseFloat((subtotal + this.shippingCost + tax - this.discountAmount).toFixed(2)); + } + + applyDiscount(): void { + const validDiscountCodes: { [key: string]: number } = { + 'SAVE10': 10, + 'WELCOME20': 20, + 'LOYAL15': 15 + }; + + if (this.discountCode && validDiscountCodes[this.discountCode]) { + this.discountAmount = (this.getSubtotal() * validDiscountCodes[this.discountCode]) / 100; + this.discountApplied = true; + } else { + this.discountAmount = 0; + this.discountApplied = false; + } + } + + canProceed(): boolean { + return this.cartItems.length > 0 && this.shippingForm.valid; + } + + proceedToPayment(): void { + if (this.canProceed()) { + const shippingData = this.shippingForm.value; + localStorage.setItem('shippingDetails', JSON.stringify(shippingData)); + this.currentStep = 'payment'; + this.updateBillingAddress(); + } else { + Object.keys(this.shippingForm.controls).forEach(key => { + this.shippingForm.get(key)?.markAsTouched(); + }); + } + } + + selectPaymentMethod(methodId: string): void { + this.selectedPaymentMethod = methodId; + } + + getSelectedPaymentMethod(): PaymentMethod | undefined { + return this.paymentMethods.find(method => method.id === this.selectedPaymentMethod); + } + + updateBillingAddress(): void { + if (this.sameAsShipping) { + const shippingData = this.shippingForm.value; + this.billingForm.patchValue({ + firstName: shippingData.firstName, + lastName: shippingData.lastName, + address: shippingData.address, + city: shippingData.city, + country: shippingData.country, + postalCode: shippingData.postalCode + }); + } + } + + canCompleteOrder(): boolean { + if (this.selectedPaymentMethod === 'credit_card') { + return this.paymentForm.valid; + } + return true; // For other payment methods that don't require form validation + } + + completeOrder(): void { + if (this.canCompleteOrder()) { + // In a real app, you would send the order to your backend here + const orderData = { + orderNumber: this.orderNumber, + date: new Date(), + items: this.cartItems, + shipping: this.shippingForm.value, + billing: this.sameAsShipping ? this.shippingForm.value : this.billingForm.value, + payment: { + method: this.selectedPaymentMethod, + details: this.selectedPaymentMethod === 'credit_card' ? this.paymentForm.value : null + }, + subtotal: this.getSubtotal(), + shippingCost: this.shippingCost, + tax: this.calculateTax(), + discount: this.discountAmount, + total: this.getTotal() + }; + + // Save order to localStorage (in a real app, you would send to backend) + localStorage.setItem('currentOrder', JSON.stringify(orderData)); + + // Clear cart + localStorage.removeItem('cart'); + this.cartItems = []; + + // Move to confirmation step + this.currentStep = 'confirmation'; + } else { + if (this.selectedPaymentMethod === 'credit_card') { + Object.keys(this.paymentForm.controls).forEach(key => { + this.paymentForm.get(key)?.markAsTouched(); + }); + } + } + } + + backToSummary(): void { + this.currentStep = 'summary'; + } +} \ No newline at end of file diff --git a/src/app/components/vendor-page/cart/cart.component.css b/src/app/components/vendor-page/cart/cart.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/vendor-page/cart/cart.component.html b/src/app/components/vendor-page/cart/cart.component.html new file mode 100644 index 0000000..feb3f05 --- /dev/null +++ b/src/app/components/vendor-page/cart/cart.component.html @@ -0,0 +1,142 @@ +
+ +
+
+
+ + + +
+
+ + +
+
+
+ Your Shopping Cart +
+

+ Carefully selected for you, +

+

+ {{ user.name }} +

+

Review your selections

+
+
+
+ + +
+
+

+ Items in Your Cart +

+ +
+
+ {{ item.name }} +

{{ item.name }}

+

{{ item.subtitle }}

+

+ Category: {{ item.category }} +

+

+ ${{ item.price.toFixed(2) }} +

+
+
+ Qty: +

+ {{ item.quantity }} +

+
+ +
+
+
+ + +
+
+
+ Subtotal: + ${{ getSubtotal() }} +
+
+ Shipping: + ${{ getShipping() }} +
+
+ Total: + ${{ getTotal() }} +
+
+ +
+ + + +
+
+ + + +
+

Your cart is currently empty.

+ + Browse Products + +
+
+
+
+ + +
+
+ © 2025 Vendor Ordering System. All rights reserved. +
+
+
\ No newline at end of file diff --git a/src/app/components/vendor-page/cart/cart.component.spec.ts b/src/app/components/vendor-page/cart/cart.component.spec.ts new file mode 100644 index 0000000..03dcc4a --- /dev/null +++ b/src/app/components/vendor-page/cart/cart.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CartComponent } from './cart.component'; + +describe('CartComponent', () => { + let component: CartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CartComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/cart/cart.component.ts b/src/app/components/vendor-page/cart/cart.component.ts new file mode 100644 index 0000000..2c22934 --- /dev/null +++ b/src/app/components/vendor-page/cart/cart.component.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-cart', + standalone: true, + imports: [CommonModule, RouterLink, FormsModule], + templateUrl: './cart.component.html', +}) + + +export class CartComponent implements OnInit { + user = { + name: "Alice Fernando", + role: "Tea Supplier", + }; + + cartItems: any[] = []; + shippingCost: number = 5.99; + + constructor(private router: Router) {} + + ngOnInit(): void { + this.loadCartItems(); + } + + loadCartItems(): void { + this.cartItems = JSON.parse(localStorage.getItem("cart") || '[]'); + } + + removeFromCart(index: number): void { + this.cartItems.splice(index, 1); + this.saveCartToStorage(); + } + + updateQuantity(index: number, quantity: number): void { + if (quantity > 0) { + this.cartItems[index].quantity = quantity; + this.saveCartToStorage(); + } + } + + saveCartToStorage(): void { + localStorage.setItem("cart", JSON.stringify(this.cartItems)); + } + + getSubtotal(): string { + let subtotal = 0; + this.cartItems.forEach((item: any) => { + subtotal += item.price * (item.quantity || 1); + }); + return subtotal.toFixed(2); + } + + getShipping(): string { + return this.cartItems.length > 0 ? this.shippingCost.toFixed(2) : '0.00'; + } + + getTotal(): string { + const subtotal = parseFloat(this.getSubtotal()); + const shipping = parseFloat(this.getShipping()); + return (subtotal + shipping).toFixed(2); + } + + proceedToCheckout(): void { + if (this.cartItems.length > 0) { + this.router.navigate(['order-summery']); + } + } + + continueShopping(): void { + this.router.navigate(['/products']); + } +} \ No newline at end of file diff --git a/src/app/components/vendor-page/order-summary/order-summary.component.css b/src/app/components/vendor-page/order-summary/order-summary.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/vendor-page/order-summary/order-summary.component.html b/src/app/components/vendor-page/order-summary/order-summary.component.html new file mode 100644 index 0000000..0439921 --- /dev/null +++ b/src/app/components/vendor-page/order-summary/order-summary.component.html @@ -0,0 +1,180 @@ +
+ +
+
+
+ + + +
+
+ + +
+
+
+ Order Summary +
+

Thanks for your order,

+

{{ user.name }}

+

Here's a summary of your purchase

+
+
+
+ + +
+
+ +
+
+

Order Status

+ {{ order.status }} +
+ +
+ +
+
+
+ + +
+
+
+ + {{ i + 1 }} +
+ {{ step }} +
+
+
+
+ + +
+ +
+

Your Items

+ +
+
+ {{ item.name }} +
+
+

{{ item.name }}

+

+ ${{ (item.price * item.quantity).toFixed(2) }} +

+
+

{{ item.description }}

+
+ Qty: {{ item.quantity }} + Unit Price: ${{ item.price.toFixed(2) }} +
+
+ {{ item.status }} +
+
+
+
+
+ + +
+
+

Order Details

+ +
+

+ Order ID: + {{ order.id }} +

+

+ Date: + {{ getFormattedDate() }} +

+

+ Estimated Delivery: + {{ getEstimatedDelivery() }} +

+
+ +
+

Shipping Address

+

{{ order.shippingAddress }}

+
+ +
+ +
+

+ Subtotal: + ${{ getSubtotal() }} +

+

+ Shipping: + ${{ order.shippingCost.toFixed(2) }} +

+

+ Tax: + ${{ order.tax.toFixed(2) }} +

+
+ +
+ Total: + ${{ getTotal() }} +
+ +
+ + +
+
+
+
+ + + +
+
+ + + +
+

You don't have any orders yet.

+ + Start Shopping + +
+
+
+
+ + +
+
+ © 2025 Vendor Ordering System. All rights reserved. +
+
+
\ No newline at end of file diff --git a/src/app/components/vendor-page/order-summary/order-summary.component.spec.ts b/src/app/components/vendor-page/order-summary/order-summary.component.spec.ts new file mode 100644 index 0000000..3fe9901 --- /dev/null +++ b/src/app/components/vendor-page/order-summary/order-summary.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OrderSummaryComponent } from './order-summary.component'; + +describe('OrderSummaryComponent', () => { + let component: OrderSummaryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OrderSummaryComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrderSummaryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/order-summary/order-summary.component.ts b/src/app/components/vendor-page/order-summary/order-summary.component.ts new file mode 100644 index 0000000..3817bb2 --- /dev/null +++ b/src/app/components/vendor-page/order-summary/order-summary.component.ts @@ -0,0 +1,132 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-order-summary', + standalone: true, + imports: [CommonModule, RouterLink], + templateUrl: './order-summary.component.html', + styleUrl: './order-summary.component.css', +}) +export class OrderSummaryComponent implements OnInit { + user = { + name: 'Prabath', + email: 'prabath@example.com', + role: 'Customer' + }; + + order = { + id: 'ORD123456', + date: '2025-05-07', + shippingAddress: '123 Main Street, Colombo, Sri Lanka', + status: 'Processing', + shippingCost: 5.99, + tax: 2.50, + estimatedDelivery: '2025-05-14' + }; + + orderItems = [ + { + name: 'Ceylon Cinnamon', + quantity: 2, + price: 10.0, + image: 'path/to/image1.jpg', + description: 'Premium quality Ceylon cinnamon sticks, 100g package', + status: 'Processing' + }, + { + name: 'Black Pepper', + quantity: 1, + price: 5.5, + image: 'path/to/image2.jpg', + description: 'Organic black pepper, 50g package', + status: 'Processing' + }, + ]; + + // Order progress tracking + orderSteps = ['Confirmed', 'Processing', 'Shipped', 'Delivered']; + currentStep = 1; // 0-based index (1 = Processing) + + constructor(private router: Router) {} + + ngOnInit(): void { + // Load order data from localStorage if available + const savedOrder = localStorage.getItem('currentOrder'); + if (savedOrder) { + try { + const orderData = JSON.parse(savedOrder); + if (orderData.items && orderData.items.length > 0) { + this.orderItems = orderData.items; + } + + // Clear cart after successful order + localStorage.removeItem('cart'); + } catch (error) { + console.error('Error parsing order data:', error); + } + } + } + + getSubtotal(): string { + return this.orderItems + .reduce((acc, item) => acc + item.price * item.quantity, 0) + .toFixed(2); + } + + getTotal(): string { + const subtotal = parseFloat(this.getSubtotal()); + const shipping = this.order.shippingCost; + const tax = this.order.tax; + + return (subtotal + shipping + tax).toFixed(2); + } + + getFormattedDate(): string { + const date = new Date(this.order.date); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + + getEstimatedDelivery(): string { + const date = new Date(this.order.estimatedDelivery); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + + getProgressWidth(): string { + const progress = ((this.currentStep + 1) / this.orderSteps.length) * 100; + return `${progress}%`; + } + + getStatusClass(status: string): string { + const baseClasses = 'px-3 py-1 rounded-full text-xs font-medium'; + + switch(status.toLowerCase()) { + case 'processing': + return `${baseClasses} bg-blue-100 text-blue-800`; + case 'shipped': + return `${baseClasses} bg-purple-100 text-purple-800`; + case 'delivered': + return `${baseClasses} bg-green-100 text-green-800`; + case 'cancelled': + return `${baseClasses} bg-red-100 text-red-800`; + default: + return `${baseClasses} bg-gray-100 text-gray-800`; + } + } + + trackOrder(): void { + // Implementation for order tracking functionality + // This could navigate to a dedicated tracking page + alert(`Tracking order ${this.order.id}...`); + } +} \ No newline at end of file diff --git a/src/app/components/vendor-page/orders/orders.component.css b/src/app/components/vendor-page/orders/orders.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/vendor-page/orders/orders.component.html b/src/app/components/vendor-page/orders/orders.component.html new file mode 100644 index 0000000..c386993 --- /dev/null +++ b/src/app/components/vendor-page/orders/orders.component.html @@ -0,0 +1,230 @@ +
+ +
+
+
+ + + +
+
+ + +
+
+
+ Order History +
+

Your purchase history,

+

{{ user.name }}

+

Track and manage your previous orders

+
+
+
+ + +
+
+ +
+
+

Your Orders

+ + {{ orders.length }} orders + +
+ +
+ +
+ +
+ + + +
+
+ + + + + + +
+
+ + +
+
+ + + + +
+
+ +
+
+
+

Order #{{ order.id }}

+

{{ formatDate(order.date) }}

+
+ + {{ order.status }} + +
+

Items: {{ order.itemsCount }}

+

Total: ${{ order.total.toFixed(2) }}

+
+ + +
+
+ + + +
+
+
+ + +
+
+ Showing {{ paginationStart }} - {{ paginationEnd }} of {{ orders.length }} orders +
+
+ + +
+ +
+ + +
+
+
+ + + +
+
+ + + +
+

No orders found

+

+ {{ orders.length > 0 ? 'No orders match your current filters.' : 'You haven\'t placed any orders yet.' }} +

+
+ + + Browse Products + +
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/components/vendor-page/orders/orders.component.spec.ts b/src/app/components/vendor-page/orders/orders.component.spec.ts new file mode 100644 index 0000000..4f12d69 --- /dev/null +++ b/src/app/components/vendor-page/orders/orders.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OrdersComponent } from './orders.component'; + +describe('OrdersComponent', () => { + let component: OrdersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OrdersComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrdersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/orders/orders.component.ts b/src/app/components/vendor-page/orders/orders.component.ts new file mode 100644 index 0000000..cdb2cdb --- /dev/null +++ b/src/app/components/vendor-page/orders/orders.component.ts @@ -0,0 +1,191 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { OrderService, OrderResponse } from '../../../services/order.service'; + +interface Order { + id: string; + date: Date; + itemsCount: number; + total: number; + status: 'processing' | 'shipped' | 'delivered' | 'cancelled'; + trackingNumber?: string; +} + +interface User { + name: string; +} + +@Component({ + selector: 'app-orders', + standalone: true, + imports: [CommonModule, FormsModule, RouterModule], + templateUrl: './orders.component.html', + styleUrl: './orders.component.css' +}) +export class OrdersComponent implements OnInit { + user: User = { name: 'John Doe' }; + + orders: Order[] = []; + filteredOrders: Order[] = []; + + currentPage = 1; + itemsPerPage = 5; + totalPages = 1; + pageNumbers: number[] = []; + paginationStart = 0; + paginationEnd = 0; + + searchQuery = ''; + statusFilter = 'all'; + dateFilter = 'all'; + + constructor(private orderService: OrderService) {} + + ngOnInit(): void { + this.loadOrdersFromServer(2); // You can pass vendor ID dynamically + } + + loadOrdersFromServer(vendorId: number): void { + this.orderService.getOrdersByVendorId(vendorId).subscribe({ + next: (data: OrderResponse[]) => { + this.orders = data.map(order => ({ + id: order.order_id.toString(), + date: new Date(order.created_at), + itemsCount: order.products.reduce((sum, p) => sum + p.count, 0), + total: order.products.reduce((sum, p) => sum + (p.unit_price * p.count), 0), + status: this.mapStatus(order.status), + trackingNumber: order.blockchain_tx_id + })); + this.filterOrders(); + this.calculatePagination(); + }, + error: err => { + console.error('Error fetching orders:', err); + } + }); + } + + mapStatus(apiStatus: string): 'processing' | 'shipped' | 'delivered' | 'cancelled' { + const status = apiStatus.toLowerCase(); + if (status.includes('pending')) return 'processing'; + if (status.includes('shipped')) return 'shipped'; + if (status.includes('delivered')) return 'delivered'; + if (status.includes('cancelled')) return 'cancelled'; + return 'processing'; + } + + filterOrders(): void { + let filtered = [...this.orders]; + + // Apply search filter + if (this.searchQuery.trim()) { + const query = this.searchQuery.toLowerCase().trim(); + filtered = filtered.filter(order => + order.id.toLowerCase().includes(query) + ); + } + + // Apply status filter + if (this.statusFilter !== 'all') { + filtered = filtered.filter(order => + order.status === this.statusFilter + ); + } + + // Apply date filter + if (this.dateFilter !== 'all') { + const today = new Date(); + const daysAgo = parseInt(this.dateFilter); + const cutoffDate = new Date(); + cutoffDate.setDate(today.getDate() - daysAgo); + + filtered = filtered.filter(order => + order.date >= cutoffDate + ); + } + + this.filteredOrders = filtered; + this.currentPage = 1; // Reset to first page when filters change + this.calculatePagination(); + } + + calculatePagination(): void { + this.totalPages = Math.ceil(this.filteredOrders.length / this.itemsPerPage); + + // Generate page numbers array + this.pageNumbers = []; + for (let i = 1; i <= this.totalPages; i++) { + this.pageNumbers.push(i); + } + + // Calculate items showing + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + this.paginationStart = startIndex + 1; + this.paginationEnd = Math.min(startIndex + this.itemsPerPage, this.filteredOrders.length); + } + + prevPage(): void { + if (this.currentPage > 1) { + this.currentPage--; + this.calculatePagination(); + } + } + + nextPage(): void { + if (this.currentPage < this.totalPages) { + this.currentPage++; + this.calculatePagination(); + } + } + + goToPage(page: number): void { + this.currentPage = page; + this.calculatePagination(); + } + + resetFilters(): void { + this.searchQuery = ''; + this.statusFilter = 'all'; + this.dateFilter = 'all'; + this.filterOrders(); + } + + canTrack(order: Order): boolean { + return (order.status === 'shipped' || order.status === 'processing') && + !!order.trackingNumber; + } + + trackOrder(orderId: string): void { + // This would typically navigate to a tracking page or open a modal + const order = this.orders.find(o => o.id === orderId); + if (order?.trackingNumber) { + alert(`Tracking order #${orderId} with tracking number: ${order.trackingNumber}`); + // In a real application, you would implement proper tracking functionality + } + } + + formatDate(date: Date): string { + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + } + + getStatusClass(status: string): string { + switch (status) { + case 'processing': + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800'; + case 'shipped': + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800'; + case 'delivered': + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800'; + case 'cancelled': + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800'; + default: + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800'; + } + } +} \ No newline at end of file diff --git a/src/app/components/vendor-page/product-section/product-section.component.css b/src/app/components/vendor-page/product-section/product-section.component.css new file mode 100644 index 0000000..f86e874 --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.css @@ -0,0 +1,25 @@ +/* Add to product-section.component.css */ +.animate-fade-in { + animation: fadeIn 0.3s ease-in-out; +} + +.animate-slide-up { + animation: slideUp 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + diff --git a/src/app/components/vendor-page/product-section/product-section.component.html b/src/app/components/vendor-page/product-section/product-section.component.html new file mode 100644 index 0000000..fd4ccad --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.html @@ -0,0 +1,168 @@ +
+
+ +
+
+
+ + + +
+
+ + +
+ + + +
+
+ Premium Spice Collection +
+

+ Discover Our Authentic Flavors +

+

+ Handcrafted blends from around the world +

+
+
+
+ + +
+ @for (category of categories; track category) { + + + } +
+ + +
+ @for (product of filteredProducts; track product.name) { +
+
+ +
+
+

+ {{ product.name }} +

+

{{ product.subtitle }}

+

+ ${{ product.price }} +

+ +
+
+ } +
+ + + @if(selectedProduct) { +
+
+ + + + +
+ +

+ {{ selectedProduct.name }} +

+

{{ selectedProduct.subtitle }}

+

+ ${{ selectedProduct.price }} +

+

+ Our spices are ethically sourced and expertly curated for + exceptional flavor in every dish. +

+
+ +
+ Quantity: +
+ + +
+ + + +
+
+
+
+
+
+
+ } +
+
\ No newline at end of file diff --git a/src/app/components/vendor-page/product-section/product-section.component.spec.ts b/src/app/components/vendor-page/product-section/product-section.component.spec.ts new file mode 100644 index 0000000..e2856b3 --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductSectionComponent } from './product-section.component'; + +describe('ProductSectionComponent', () => { + let component: ProductSectionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductSectionComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProductSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/product-section/product-section.component.ts b/src/app/components/vendor-page/product-section/product-section.component.ts new file mode 100644 index 0000000..a05da78 --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.ts @@ -0,0 +1,139 @@ +import { CommonModule } from '@angular/common'; +import { Component, signal, OnInit, OnDestroy } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { ProductService, Product, ApiProduct } from '../../../services/product.service'; + +interface CartItem { + name: string; + subtitle: string; + price: number; + image: string; + quantity: number; +} + +@Component({ + selector: 'app-product-section', + standalone: true, + imports: [ + CommonModule, + RouterLink, + FormsModule, + ], + templateUrl: './product-section.component.html', + styleUrl: './product-section.component.css', +}) + + + + + +export class ProductSectionComponent implements OnInit, OnDestroy { + categories = [ + { id: 'All', name: 'All' }, + { id: 1, name: 'Powders' }, + { id: 2, name: 'Whole Spices' }, + { id: 3, name: 'Blends' } + ]; + + + selectedCategory: string | number | 'All' = 'All'; // Default to 'All' + + + categoryMap: { [key: number]: string } = { + 1: 'Powders', + 2: 'Whole Spices', + 3: 'Blends', + }; + + selectedProduct: Product | null = null; + selectedQuantity = 1; + + // Cart state + cartCount = signal(0); + cartItems = signal([]); + + products: Product[] = []; // Store products dynamically + + constructor(private productService: ProductService) { + this.loadCart(); + } + + ngOnInit(): void { + this.productService.getProducts().subscribe((products: ApiProduct[]) => { + this.products = products.map((product) => ({ + name: product.product_name, + subtitle: 'Description', + price: +product.unit_price, + category: product.category, // keep as number + image: 'assets/images/products/spices-bg1.jpg', + })); + }); + + + // Handle keyboard events + window.addEventListener('keydown', this.handleKeydown); + } + + get filteredProducts() { + return this.selectedCategory === 'All' + ? this.products + : this.products.filter(product => product.category === this.selectedCategory); + } + + updateQuantity(value: string) { + this.selectedQuantity = +value; + } + + openProductModal(product: Product) { + this.selectedProduct = product; + this.selectedQuantity = 1; // Reset quantity when opening modal + } + + closeProductModal() { + this.selectedProduct = null; + } + + addToCart(product: Product) { + const currentCart = this.cartItems(); + const existingItem = currentCart.find(item => item.name === product.name); + + if (existingItem) { + // Update quantity if item exists + existingItem.quantity += this.selectedQuantity; + } else { + // Add new item to cart + currentCart.push({ + ...product, + quantity: this.selectedQuantity + }); + } + + // Update cart state + this.cartItems.set([...currentCart]); + this.cartCount.set(currentCart.reduce((sum) => sum + 1, 0)); + this.saveCart(); + this.closeProductModal(); + } + + private saveCart() { + localStorage.setItem('cart', JSON.stringify(this.cartItems())); + } + + private loadCart() { + const savedCart = localStorage.getItem('cart'); + if (savedCart) { + this.cartItems.set(JSON.parse(savedCart)); + this.cartCount.set(this.cartItems().reduce((sum, item) => sum + item.quantity, 0)); + } + } + + // Handle keyboard events + ngOnDestroy(): void { + window.removeEventListener('keydown', this.handleKeydown); + } + + handleKeydown = (e: KeyboardEvent) => { + if (e.key === 'Escape') this.closeProductModal(); + }; +} diff --git a/src/app/components/vendor-page/vender/vendor.component.css b/src/app/components/vendor-page/vender/vendor.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/vendor-page/vender/vendor.component.html b/src/app/components/vendor-page/vender/vendor.component.html new file mode 100644 index 0000000..6200e2d --- /dev/null +++ b/src/app/components/vendor-page/vender/vendor.component.html @@ -0,0 +1,67 @@ +
+ +
+
+
+ + + +
+
+ + +
+
+
+ Vendor Ordering Flow +
+

Welcome back,

+

{{ user.name }}

+

{{ user.role }}

+
+
+
+ + + + + +
+
+ © 2025 Vendor Ordering System. All rights reserved. +
+
+
diff --git a/src/app/components/vendor-page/vender/vendor.component.spec.ts b/src/app/components/vendor-page/vender/vendor.component.spec.ts new file mode 100644 index 0000000..c0d25f1 --- /dev/null +++ b/src/app/components/vendor-page/vender/vendor.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VendorComponent } from './vendor.component'; + +describe('VendorComponent', () => { + let component: VendorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VendorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(VendorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/vender/vendor.component.ts b/src/app/components/vendor-page/vender/vendor.component.ts new file mode 100644 index 0000000..adab49a --- /dev/null +++ b/src/app/components/vendor-page/vender/vendor.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-vendor', + templateUrl: './vendor.component.html', + styleUrls: ['./vendor.component.css'], + imports: [RouterLink], +}) +export class VendorComponent { + user = { + name: "Alice Fernando", + role: "Tea Supplier", + }; +} diff --git a/src/app/services/order.service.ts b/src/app/services/order.service.ts new file mode 100644 index 0000000..2d30792 --- /dev/null +++ b/src/app/services/order.service.ts @@ -0,0 +1,41 @@ +// src/app/services/order.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export interface Product { + product_id: number; + count: number; + unit_price: number; +} + +export interface OrderDetails { + warehouse_id: number; + nearest_city: string; + latitude: string; + longitude: string; +} + +export interface OrderResponse { + order_id: number; + user_id: number; + status: string; + blockchain_tx_id: string; + ipfs_hash: string; + created_at: string; + products: Product[]; + details: OrderDetails; +} + +@Injectable({ + providedIn: 'root' +}) +export class OrderService { + private baseUrl = 'http://127.0.0.1:8000/api/v0/orders/vendor'; + + constructor(private http: HttpClient) {} + + getOrdersByVendorId(vendorId: number): Observable { + return this.http.get(`${this.baseUrl}/${vendorId}/`); + } +} diff --git a/src/app/services/product.service.ts b/src/app/services/product.service.ts new file mode 100644 index 0000000..688d03a --- /dev/null +++ b/src/app/services/product.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export interface ApiProduct { + id: number; + product_SKU: string; + product_name: string; + + unit_price: string; + category: number; + } + +// "id": 1, +// "product_SKU": "SKU001", +// "product_name": "Cardamom Product 1", +// "unit_price": "1257.29", +// "created_at": "2025-05-07T14:36:06.635310Z", +// "updated_at": "2025-05-07T14:36:06.635332Z", +// "category": 3 + + export interface Product { + name: string; + subtitle: string; + price: number; + category: number; // keep it number + image: string; + } + + + +@Injectable({ + providedIn: 'root', +}) +export class ProductService { + private apiUrl = 'http://127.0.0.1:8000/api/product/products/'; // Your API endpoint + + constructor(private http: HttpClient) {} + + // Fetch all products from the API + getProducts(): Observable { + console.log('Fetching products from API...'); + return this.http.get(this.apiUrl); + + } + // You can implement methods for filtering and sorting if needed + // Example of category filter method + filterProductsByCategory(products: Product[], category: number): Product[] { + + return products.filter(product => product.category === category); + } + + + + +} diff --git a/src/app/services/warehouse.service.ts b/src/app/services/warehouse.service.ts new file mode 100644 index 0000000..57920a4 --- /dev/null +++ b/src/app/services/warehouse.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export interface Warehouse { + id: number; + location_x: string; + location_y: string; + warehouse_name: string; + capacity: string; + created_at: string; +} + +export interface InventoryProductDetail { + product_name: string; + category: string; + supplied_by: string; + supplied_date: string; + product_count: number; +} + +export interface WarehouseInventory { + warehouse_city: string; + capacity: number; + last_restocked: string; + current_stock_level: number; + inventory_product_details: InventoryProductDetail[]; +} + +@Injectable({ + providedIn: 'root', +}) +export class WarehouseService { + private warehousesUrl = 'http://127.0.0.1:8000/api/warehouse/warehouses/'; + private inventoryUrl = 'http://127.0.0.1:8000/api/warehouse/inventory/'; + + constructor(private http: HttpClient) {} + + // Fetch all warehouses from the API + getWarehouses(): Observable { + console.log('Fetching warehouses from API...'); + return this.http.get(this.warehousesUrl); + } + + // Fetch inventory for a specific warehouse + getWarehouseInventory(warehouseId: number): Observable { + console.log(`Fetching inventory for warehouse ID: ${warehouseId}`); + return this.http.get(`${this.inventoryUrl}?warehouse_id=${warehouseId}`); + } +} \ No newline at end of file