diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 897d24f..777b6ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "@angular/router": "^19.2.0", "@tailwindcss/postcss": "^4.1.4", "chart.js": "^4.4.9", + "dotenv": "^16.5.0", "jwt-decode": "^4.0.0", + "leaflet": "^1.9.4", "lucide-angular": "^0.503.0", "ng2-charts": "^8.0.0", "postcss": "^8.5.3", @@ -31,7 +33,9 @@ "@angular-devkit/build-angular": "^19.2.8", "@angular/cli": "^19.2.8", "@angular/compiler-cli": "^19.2.0", + "@types/google.maps": "^3.58.1", "@types/jasmine": "~5.1.0", + "@types/leaflet": "^1.9.17", "jasmine-core": "~5.6.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -5082,6 +5086,20 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -5109,6 +5127,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/leaflet": { + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz", + "integrity": "sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -6948,6 +6976,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -9170,6 +9210,12 @@ "shell-quote": "^1.8.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/less": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", diff --git a/package.json b/package.json index 92bc026..2f565b3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "@angular/router": "^19.2.0", "@tailwindcss/postcss": "^4.1.4", "chart.js": "^4.4.9", + "dotenv": "^16.5.0", "jwt-decode": "^4.0.0", + "leaflet": "^1.9.4", "lucide-angular": "^0.503.0", "ng2-charts": "^8.0.0", "postcss": "^8.5.3", @@ -33,7 +35,9 @@ "@angular-devkit/build-angular": "^19.2.8", "@angular/cli": "^19.2.8", "@angular/compiler-cli": "^19.2.0", + "@types/google.maps": "^3.58.1", "@types/jasmine": "~5.1.0", + "@types/leaflet": "^1.9.17", "jasmine-core": "~5.6.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", 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 9f8119d..46f5b45 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -21,6 +21,13 @@ import { TransactionsComponent } from './components/warehouse-manager-page/trans import { TruckTrackingComponent } from './components/warehouse-manager-page/truck-tracking/truck-tracking.component'; import { VendorOrdersComponent } from './components/warehouse-manager-page/vendor-orders/vendor-orders.component'; import { SupplierReqSurveyComponent } from './components/warehouse-manager-page/supplier-req-survey/supplier-req-survey.component'; +import {WarehouseComponent} from './components/admin-page/warehouse/warehouse.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 { CreateOrderComponent } from './components/vendor-page/create-order/create-order.component'; export const routes: Routes = [ { @@ -70,7 +77,9 @@ export const routes: Routes = [ canActivate: [roleGuard(1)], children : [ { path : '', redirectTo: 'profile', pathMatch: 'full'}, + { path : 'profile', component: ProfileComponent, pathMatch: 'full'}, + { path: 'warehouse', component: WarehouseComponent, pathMatch: 'full' }, { path: 'forecast', component: ForecastComponent, @@ -101,7 +110,14 @@ export const routes: Routes = [ path : 'vendor', canActivate : [roleGuard(4)], children : [ - { path : '', redirectTo: 'profile', pathMatch: 'full'}, + { path : '', redirectTo: 'product-section', pathMatch: 'full'}, + {path : 'product-section', component: ProductSectionComponent, pathMatch: 'full'}, + {path : 'cart', component: CartComponent, pathMatch: 'full'}, + {path : 'orders', component: OrdersComponent, pathMatch: 'full'}, + {path : 'order-summary', component: OrderSummaryComponent, pathMatch: 'full'}, + {path : 'cart-summary', component: CartSummaryComponent, pathMatch: 'full'}, + {path : 'create-order', component: CreateOrderComponent, pathMatch: 'full'}, + { path : 'profile', component: ProfileComponent, pathMatch: 'full'}, // rest... (modify sidebar.ts as well) { path: '**', redirectTo: '', pathMatch: 'full' } ] 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..f124818 --- /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 '../../../service/warehouse/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/vendor-page/cart-summary/cart-summary.component.css b/src/app/components/vendor-page/cart-summary/cart-summary.component.css new file mode 100644 index 0000000..4f59042 --- /dev/null +++ b/src/app/components/vendor-page/cart-summary/cart-summary.component.css @@ -0,0 +1,60 @@ +/* Add this to your existing CSS */ + +/* Map container styling */ +app-map-selector { + display: block; + border-radius: 0.5rem; + overflow: hidden; +} + +/* Selected location badge styles */ +.location-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: rgba(0, 95, 115, 0.1); + border-radius: 0.5rem; + color: #005f73; + font-size: 0.875rem; + font-weight: 500; + margin-top: 1rem; +} + +/* Map progress animation */ +@keyframes pulse { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.7; } + 100% { transform: scale(1); opacity: 1; } +} + +.map-loading { + animation: pulse 1.5s infinite; +} + +/* Order confirmation map preview */ +.confirmation-map-preview { + width: 100%; + height: 120px; + border-radius: 0.375rem; + overflow: hidden; + margin-top: 0.5rem; + border: 1px solid rgba(0, 95, 115, 0.2); +} + +/* Add to cart-summary.component.css */ +.delivery-map-container { + background-color: #f9f9f9; + padding: 1rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + margin-top: 2rem; +} + +.location-details { + margin-top: 1rem; + padding: 0.75rem; + background-color: #ffffff; + border-radius: 6px; + border-left: 4px solid #4285f4; +} \ No newline at end of file diff --git a/src/app/components/vendor-page/cart-summary/cart-summary.component.html b/src/app/components/vendor-page/cart-summary/cart-summary.component.html new file mode 100644 index 0000000..2acb72a --- /dev/null +++ b/src/app/components/vendor-page/cart-summary/cart-summary.component.html @@ -0,0 +1,463 @@ +
+ +
+ + + +
+ +
+
+ + +
+
+
+ + + +
+
+ + +
+
+
+ Cart Summary + Delivery Location + Payment + Order Confirmation +
+

Checkout

+

+ Review your items + Choose your delivery location + Complete your purchase + Your order is confirmed +

+
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+
+ + + +
+ Cart +
+ +
+
+ 2 + + + +
+ Location +
+ +
+
+ 3 + + + +
+ Payment +
+ +
+
+ 4 + + + +
+ Complete +
+
+
+
+ + +
+ +
+ +
+ +
+
+

Your Items

+ + {{ getTotalItems() }} + +
+ +
+ Your cart is empty +
+ +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+
+ + +
+
+ +
+
+
+

Select Delivery Location

+

Move the marker or click on the map to set your exact delivery location

+
+ +
+ +
+ +
+ + +
+

+ + + + + Delivery Address Details +

+ +
+
+ + +
+ First name is required +
+
+
+ + +
+ Last name is required +
+
+
+ +
+ + +
+ Valid phone number is required +
+
+ +
+ + +
+ Address is required +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + +
+
+
+ + + + +
+
+

Selected Location

+

+ Latitude: {{ selectedLat | number:'1.6-6' }}, Longitude: {{ selectedLng | number:'1.6-6' }} +

+

+ {{ locationAddress }} +

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

+ Order Summary +

+ +
+
+

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

+

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

+

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

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

+ + + + Delivery Information +

+

+ Select your exact location on the map to ensure accurate delivery. Our system will track your order in real-time. +

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

Order Confirmed!

+

Your order has been placed and is being processed

+ +
+

Order #: {{ orderNumber }}

+

{{ orderDate | date:'medium' }}

+ + +
+

+ + + + + Delivery Location +

+

+ {{ deliveryForm.get('address')?.value }}
+ {{ deliveryForm.get('city')?.value }}, {{ deliveryForm.get('state')?.value }} {{ deliveryForm.get('zipCode')?.value }} +

+

+ Coordinates: {{ selectedLat | number:'1.6-6' }}, {{ selectedLng | number:'1.6-6' }} +

+
+ + +
+

+ + + + Track Your Order +

+

+ Use the link below to track your order in real-time +

+ + View Order Status + +
+ +
+ + +
+

Delivery Location

+
+
+

Coordinates: Lat: {{selectedLat?.toFixed(6)}}, Lng: {{selectedLng?.toFixed(6)}}

+

Address: {{locationAddress}}

+
+
+
+
+ + +
+
+
+ +
+
+
\ 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..9c51439 --- /dev/null +++ b/src/app/components/vendor-page/cart-summary/cart-summary.component.ts @@ -0,0 +1,370 @@ +import { Component, OnInit, HostListener, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { MapSelectorComponent } from '../map-selector/map-selector.component'; +import * as L from 'leaflet'; + +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, MapSelectorComponent] +}) +export class CartSummaryComponent implements OnInit, AfterViewInit { + currentStep: 'summary' | 'delivery' | 'payment' | 'confirmation' = 'summary'; + cartItems: CartItem[] = []; + shippingForm: FormGroup; + deliveryForm: FormGroup; + billingForm: FormGroup; + paymentForm: FormGroup; + + // Map and location data + selectedLat: number | null = null; + selectedLng: number | null = null; + locationAddress: string | null = null; + + // Add ViewChild for the confirmation map + @ViewChild('confirmationMap') confirmationMapElement!: ElementRef; + + // Add map property + confirmationMap?: L.Map; + confirmationMarker?: L.Marker; + + 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(); + + // UI state + isCartPreviewOpen: boolean = false; + isItemsCollapsed: boolean = false; + isOrderSummaryItemsVisible: boolean = false; + + constructor( + private fb: FormBuilder, + private router: Router + ) { + // Initialize delivery form specifically for map-based delivery + this.deliveryForm = this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9]{10,15}$/)]], + address: ['', Validators.required], + city: ['', Validators.required], + state: ['', Validators.required], + zipCode: ['', [Validators.required, Validators.pattern('^[0-9]{5}(?:-[0-9]{4})?$')]], + instructions: [''], + latitude: [null], + longitude: [null] + }); + + // Initialize shipping form + this.shippingForm = this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + address: ['', Validators.required], + city: ['', Validators.required], + state: ['', Validators.required], + zipCode: ['', [Validators.required, Validators.pattern('^[0-9]{5}(?:-[0-9]{4})?$')]], + country: ['United States', Validators.required] + }); + + // Initialize billing form + this.billingForm = this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + address: ['', Validators.required], + city: ['', Validators.required], + state: ['', Validators.required], + zipCode: ['', [Validators.required, Validators.pattern('^[0-9]{5}(?:-[0-9]{4})?$')]], + country: ['United States', Validators.required] + }); + + // Initialize payment form + 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(); + + // Ensure Leaflet marker icons work properly + if (!L.Icon.Default.imagePath) { + delete (L.Icon.Default.prototype as any)._getIconUrl; + + L.Icon.Default.mergeOptions({ + iconRetinaUrl: 'assets/marker-icon-2x.png', + iconUrl: 'assets/marker-icon.png', + shadowUrl: 'assets/marker-shadow.png', + }); + } + } + + ngAfterViewInit(): void { + // If we're in confirmation step right on load, initialize map + // (normally this happens in completeOrder) + if (this.currentStep === 'confirmation' && this.selectedLat && this.selectedLng) { + this.initConfirmationMap(); + } + } + + // Initialize confirmation map + initConfirmationMap(): void { + // Wait for the DOM to be ready + setTimeout(() => { + if (this.confirmationMapElement && this.selectedLat && this.selectedLng) { + // Create the map + this.confirmationMap = L.map(this.confirmationMapElement.nativeElement).setView( + [this.selectedLat, this.selectedLng], 15 + ); + + // Add tile layer + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(this.confirmationMap); + + // Add marker for delivery location + this.confirmationMarker = L.marker([this.selectedLat, this.selectedLng]) + .addTo(this.confirmationMap) + .bindPopup('Your delivery location') + .openPopup(); + } + }, 100); + } + + loadCartItems(): void { + const savedCart = localStorage.getItem('cart'); + this.cartItems = savedCart ? JSON.parse(savedCart) : []; + } + + loadUserData(): void { + const savedUser = localStorage.getItem('user'); + if (savedUser) { + this.user = JSON.parse(savedUser); + } + } + + generateOrderNumber(): void { + const timestamp = new Date().getTime().toString().slice(-6); + const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); + this.orderNumber = `ORD-${timestamp}-${random}`; + } + + toggleCartPreview(): void { + this.isCartPreviewOpen = !this.isCartPreviewOpen; + } + + toggleItemsCollapse(): void { + this.isItemsCollapsed = !this.isItemsCollapsed; + } + + toggleOrderSummaryItems(): void { + this.isOrderSummaryItemsVisible = !this.isOrderSummaryItemsVisible; + } + + // UPDATED: Enhanced location selection method + onLocationSelected(coords: { lat: number; lng: number }): void { + this.selectedLat = coords.lat; + this.selectedLng = coords.lng; + + // Update the form with coordinates + this.deliveryForm.patchValue({ + latitude: coords.lat, + longitude: coords.lng + }); + + // Optionally: Try to reverse geocode to get address + this.getAddressFromCoordinates(coords.lat, coords.lng); + } + + // UPDATED: Enhanced address lookup with loading state + getAddressFromCoordinates(lat: number, lng: number): void { + // In a real application, you would make an API call to a geocoding service + // For now, we'll just simulate this with a placeholder + + // Simulate a loading state + this.locationAddress = 'Loading address...'; + + // Simulate API call delay + setTimeout(() => { + this.locationAddress = 'Location near Colombo, Sri Lanka'; + }, 1000); + } + + // Close dropdown when clicking elsewhere + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent): void { + const cartPreviewElement = (event.target as HTMLElement).closest('.cart-dropdown'); + const cartToggleButton = (event.target as HTMLElement).closest('.cart-toggle'); + + if (!cartPreviewElement && !cartToggleButton && this.isCartPreviewOpen) { + this.isCartPreviewOpen = false; + } + } + + getTotalItems(): number { + return this.cartItems.reduce((total, item) => total + item.quantity, 0); + } + + getSubtotal(): string { + const subtotal = this.cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0); + return subtotal.toFixed(2); + } + + calculateTax(): number { + const subtotal = parseFloat(this.getSubtotal()); + return (subtotal * this.taxRate) / 100; + } + + getTotal(): string { + const subtotal = parseFloat(this.getSubtotal()); + const tax = this.calculateTax(); + const total = subtotal + tax + this.shippingCost - this.discountAmount; + return total.toFixed(2); + } + + applyDiscount(): void { + // Example discount codes + const discountCodes = { + 'SAVE10': 10, + 'WELCOME15': 15, + 'SPRING20': 20 + }; + + const code = this.discountCode.toUpperCase(); + + // Check if valid discount code + if (code && Object.keys(discountCodes).includes(code)) { + const percentage = discountCodes[code as keyof typeof discountCodes]; + const subtotal = parseFloat(this.getSubtotal()); + this.discountAmount = (subtotal * percentage) / 100; + this.discountApplied = true; + + // Show success alert or notification + alert(`Discount of ${percentage}% applied successfully!`); + } else { + this.discountApplied = false; + this.discountAmount = 0; + + // Show error alert or notification + alert('Invalid discount code. Please try another code.'); + } + } + + 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)); + } + + proceedToDelivery(): void { + if (this.cartItems.length > 0) { + this.currentStep = 'delivery'; + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + } + + canProceedFromDelivery(): boolean { + return this.deliveryForm.valid && this.selectedLat !== null && this.selectedLng !== null; + } + + proceedToPayment(): void { + if (this.canProceedFromDelivery()) { + this.currentStep = 'payment'; + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + } + + completeOrder(): void { + if (this.paymentForm.valid && this.cartItems.length > 0) { + // Here we would typically submit the order to the backend + // Include the delivery location coordinates + + const orderData = { + items: this.cartItems, + delivery: { + ...this.deliveryForm.value, + coordinates: { + lat: this.selectedLat, + lng: this.selectedLng + } + }, + payment: this.paymentForm.value, + total: parseFloat(this.getTotal()), + orderNumber: this.orderNumber + }; + + console.log('Order data to submit:', orderData); + + // For demo purposes, we just proceed to confirmation + this.currentStep = 'confirmation'; + + // Clear cart after successful order + this.cartItems = []; + localStorage.removeItem('cart'); + + // Initialize the confirmation map to show delivery location + this.initConfirmationMap(); + + // Scroll to top + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + } + + continueShopping(): void { + this.router.navigate(['/dashboard/vendor/products']); + } +} \ 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..93fd4bb --- /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 + +
s +
+
+
+ + +
+
+ © 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/create-order/create-order.component.css b/src/app/components/vendor-page/create-order/create-order.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/vendor-page/create-order/create-order.component.html b/src/app/components/vendor-page/create-order/create-order.component.html new file mode 100644 index 0000000..d7181ce --- /dev/null +++ b/src/app/components/vendor-page/create-order/create-order.component.html @@ -0,0 +1,50 @@ +
+

Create Order

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ 📍 Selected Location: +
+ Latitude: {{ selectedLat }} +
+ Longitude: {{ selectedLng }} +
+ + +
+ +
+ {{ responseMessage }} +
+ +
+ {{ errorMessage }} +
+
diff --git a/src/app/components/vendor-page/create-order/create-order.component.spec.ts b/src/app/components/vendor-page/create-order/create-order.component.spec.ts new file mode 100644 index 0000000..c737c2c --- /dev/null +++ b/src/app/components/vendor-page/create-order/create-order.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateOrderComponent } from './create-order.component'; + +describe('CreateOrderComponent', () => { + let component: CreateOrderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CreateOrderComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreateOrderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/create-order/create-order.component.ts b/src/app/components/vendor-page/create-order/create-order.component.ts new file mode 100644 index 0000000..8268bd4 --- /dev/null +++ b/src/app/components/vendor-page/create-order/create-order.component.ts @@ -0,0 +1,64 @@ +import { Component } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { MapSelectorComponent } from '../map-selector/map-selector.component'; + +@Component({ + selector: 'app-create-order', + standalone: true, + imports: [ReactiveFormsModule, CommonModule, MapSelectorComponent], + templateUrl: './create-order.component.html', + styleUrls: ['./create-order.component.css'] +}) +export class CreateOrderComponent { + orderForm: FormGroup; + responseMessage: string = ''; + errorMessage: string = ''; + + selectedLat: number | null = null; + selectedLng: number | null = null; + + constructor(private fb: FormBuilder, private http: HttpClient) { + this.orderForm = this.fb.group({ + order_id: [''], + status: [''], + timestamp: [''], + document: [null] + }); + } + + onFileChange(event: any) { + const file = event.target.files[0]; + this.orderForm.patchValue({ document: file }); + } + + onLocationSelected(coords: { lat: number; lng: number }) { + this.selectedLat = coords.lat; + this.selectedLng = coords.lng; + } + + submitForm() { + const formData = new FormData(); + formData.append('order_id', this.orderForm.value.order_id); + formData.append('status', this.orderForm.value.status); + formData.append('timestamp', this.orderForm.value.timestamp); + formData.append('document', this.orderForm.value.document); + + if (this.selectedLat !== null && this.selectedLng !== null) { + formData.append('latitude', this.selectedLat.toString()); + formData.append('longitude', this.selectedLng.toString()); + } + + this.http.post('http://127.0.0.1:8000/api/create-order/', formData).subscribe({ + next: (res: any) => { + this.responseMessage = `✅ Order Created! CID: ${res.cid}`; + this.errorMessage = ''; + }, + error: (err) => { + this.errorMessage = `❌ ${err.error?.error || 'Failed to create order.'}`; + this.responseMessage = ''; + } + }); + } +} diff --git a/src/app/components/vendor-page/map-selector/map-selector.component.css b/src/app/components/vendor-page/map-selector/map-selector.component.css new file mode 100644 index 0000000..b79ef73 --- /dev/null +++ b/src/app/components/vendor-page/map-selector/map-selector.component.css @@ -0,0 +1,6 @@ +/* Optional: Make map full width and responsive */ +:host { + display: block; + width: 100%; + } + \ No newline at end of file diff --git a/src/app/components/vendor-page/map-selector/map-selector.component.html b/src/app/components/vendor-page/map-selector/map-selector.component.html new file mode 100644 index 0000000..77b1f74 --- /dev/null +++ b/src/app/components/vendor-page/map-selector/map-selector.component.html @@ -0,0 +1,10 @@ +
+ + + +
+ Latitude: {{ lat }}
+ Longitude: {{ lng }} +
diff --git a/src/app/components/vendor-page/map-selector/map-selector.component.spec.ts b/src/app/components/vendor-page/map-selector/map-selector.component.spec.ts new file mode 100644 index 0000000..7d64541 --- /dev/null +++ b/src/app/components/vendor-page/map-selector/map-selector.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MapSelectorComponent } from './map-selector.component'; + +describe('MapSelectorComponent', () => { + let component: MapSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MapSelectorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MapSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/vendor-page/map-selector/map-selector.component.ts b/src/app/components/vendor-page/map-selector/map-selector.component.ts new file mode 100644 index 0000000..ce4f7d2 --- /dev/null +++ b/src/app/components/vendor-page/map-selector/map-selector.component.ts @@ -0,0 +1,93 @@ +import { Component, ElementRef, ViewChild, AfterViewInit, Output, EventEmitter, NgZone } from '@angular/core'; +import * as L from 'leaflet'; + +delete (L.Icon.Default.prototype as any)._getIconUrl; +L.Icon.Default.mergeOptions({ + iconRetinaUrl: 'assets/marker-icon-2x.png', + iconUrl: 'assets/marker-icon.png', + shadowUrl: 'assets/marker-shadow.png', +}); + +@Component({ + selector: 'app-map-selector', + standalone: true, + imports: [], + templateUrl: './map-selector.component.html', + styleUrls: ['./map-selector.component.css'] +}) +export class MapSelectorComponent implements AfterViewInit { + @ViewChild('mapDiv') mapDiv!: ElementRef; + @Output() locationSelected = new EventEmitter<{ lat: number; lng: number }>(); + + map!: L.Map; + marker!: L.Marker; + lat = 7.8731; + lng = 80.7718; + + constructor(private zone: NgZone) {} + + ngAfterViewInit(): void { + this.map = L.map(this.mapDiv.nativeElement).setView([this.lat, this.lng], 7); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(this.map); + + // Listen for map clicks + this.map.on('click', (e: L.LeafletMouseEvent) => { + const { lat, lng } = e.latlng; + this.updateMarker(lat, lng, '📍 Selected location'); + }); + } + + getUserLocation(): void { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const userLat = position.coords.latitude; + const userLng = position.coords.longitude; + + this.zone.run(() => { + this.lat = userLat; + this.lng = userLng; + }); + + this.map.setView([userLat, userLng], 15); + this.updateMarker(userLat, userLng, '📍 You are here'); + + // Optional circle to show accuracy + L.circle([userLat, userLng], { + radius: position.coords.accuracy, + color: 'blue', + fillOpacity: 0.2 + }).addTo(this.map); + + this.locationSelected.emit({ lat: userLat, lng: userLng }); + }, + (error) => { + console.warn('Geolocation failed:', error); + alert('Unable to get location. Please allow location access.'); + } + ); + } else { + alert('Geolocation not supported by your browser.'); + } + } + + private updateMarker(lat: number, lng: number, label: string): void { + if (this.marker) { + this.map.removeLayer(this.marker); + } + + this.marker = L.marker([lat, lng]).addTo(this.map) + .bindPopup(label) + .openPopup(); + + this.zone.run(() => { + this.lat = lat; + this.lng = lng; + }); + + this.locationSelected.emit({ lat, lng }); + } +} 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..6323bbf --- /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 '../../../service/order/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..d74b792 --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.css @@ -0,0 +1,26 @@ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideInRight { + from { opacity: 0; transform: translateX(30px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes slideUp { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fade-in { + animation: fadeIn 0.3s ease-out; +} + +.animate-slide-in-right { + animation: slideInRight 0.3s ease-out; +} + +.animate-slide-up { + animation: slideUp 0.4s ease-out; +} \ No newline at end of file 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..83d727f --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.html @@ -0,0 +1,199 @@ +
+
+ +
+
+
+ + + +
+
+ + +
+ + + +
+
+ Premium Spice Collection +
+

+ Discover Our Authentic Flavors +

+

+ Handcrafted blends from around the world +

+
+
+
+ + +
+ @for (notification of notifications(); track notification.id) { +
+
+ + + + + + + + + + + + + + + {{ notification.message }} +
+ +
+ } +
+ + + +
+ @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..3419d65 --- /dev/null +++ b/src/app/components/vendor-page/product-section/product-section.component.ts @@ -0,0 +1,167 @@ +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 '../../../service/warehouse/product.service'; + +interface CartItem { + name: string; + subtitle: string; + price: number; + image: string; + quantity: number; +} + +interface Notification { + message: string; + type: 'success' | 'error' | 'info'; + id: 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([]); + + // Notification state + notifications = signal([]); + private notificationIdCounter = 0; + + 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; + } + + // Method to show notifications + showNotification(message: string, type: 'success' | 'error' | 'info' = 'success') { + const id = this.notificationIdCounter++; + const notification = { message, type, id }; + + // Add notification to array + const currentNotifications = this.notifications(); + this.notifications.set([...currentNotifications, notification]); + + // Auto-dismiss after 3 seconds + setTimeout(() => { + this.dismissNotification(id); + }, 3000); + } + + dismissNotification(id: number) { + const currentNotifications = this.notifications(); + this.notifications.set(currentNotifications.filter(n => n.id !== id)); + } + + // Updated to match HTML parameters + 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, item) => sum + item.quantity, 0)); + this.saveCart(); + + // Show success notification + this.showNotification(`Added ${this.selectedQuantity} ${product.name} to cart!`); + + 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(); + }; +} \ No newline at end of file diff --git a/src/app/service/order/order.service.ts b/src/app/service/order/order.service.ts new file mode 100644 index 0000000..2d30792 --- /dev/null +++ b/src/app/service/order/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/service/warehouse/product.service.ts b/src/app/service/warehouse/product.service.ts new file mode 100644 index 0000000..688d03a --- /dev/null +++ b/src/app/service/warehouse/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/service/warehouse/warehouse.service.ts b/src/app/service/warehouse/warehouse.service.ts new file mode 100644 index 0000000..57920a4 --- /dev/null +++ b/src/app/service/warehouse/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 diff --git a/src/assets/map-pin.png b/src/assets/map-pin.png new file mode 100644 index 0000000..373e50c Binary files /dev/null and b/src/assets/map-pin.png differ diff --git a/src/assets/marker-icon-2x.png b/src/assets/marker-icon-2x.png new file mode 100644 index 0000000..88f9e50 Binary files /dev/null and b/src/assets/marker-icon-2x.png differ diff --git a/src/assets/marker-icon.png b/src/assets/marker-icon.png new file mode 100644 index 0000000..950edf2 Binary files /dev/null and b/src/assets/marker-icon.png differ diff --git a/src/assets/marker-shadow.png b/src/assets/marker-shadow.png new file mode 100644 index 0000000..9fd2979 Binary files /dev/null and b/src/assets/marker-shadow.png differ diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..8f008a6 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + googleMapsApiKey: 'your-local-google-maps-api-key' + }; + \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 6b08c22..d4f1828 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,3 +1,3 @@ @import "tailwindcss"; - +@import "leaflet/dist/leaflet.css"; body { margin: 0; padding: 0; }