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
+
+
+
+
+
+
+
+
+
0; else noWarehousesTemplate" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
+
+
+
{{ 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
+
+
+
+
+
+
+
+
0; else noInventoryTemplate">
+
+
+
+
Category
+
Product Name
+
Quantity
+
Supplied Date
+
+
+
+
+
+
+
+
+
+
{{ item.product_name }}
+
Category: {{ item.category }}
+
+
+ {{ item.product_count.toLocaleString() }} units
+
+
+
Supplied: {{ formatDate(item.supplied_date) }} by {{ item.supplied_by }}
+
+
+
+
+
{{ item.category }}
+
{{ item.product_name }}
+
+
+ {{ item.product_count.toLocaleString() }} units
+
+
+
{{ formatDate(item.supplied_date) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Your Items
+
+ {{ getTotalItems() }}
+
+
+
+
+ Your cart is empty
+
+
+
0" class="divide-y divide-slate-100">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select Delivery Location
+
Move the marker or click on the map to set your exact delivery location
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
0; else emptyCart"
+ class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8"
+ >
+
+
![{{ item.name }}]()
+
{{ item.name }}
+
{{ item.subtitle }}
+
+ Category: {{ item.category }}
+
+
+ ${{ item.price.toFixed(2) }}
+
+
+
+
Qty:
+
+ {{ item.quantity }}
+
+
+
+
+
+
+
+
+
0" class="mt-12 flex flex-col md:flex-row md:items-center md:justify-between border-t border-gray-200 pt-6">
+
+
+ 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
+
+
+
+
+ {{ 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 }}
+
+
+
+
+
+
+
+
+
+
currentStep"
+ [class.text-white]="i <= currentStep"
+ [class.text-gray-400]="i > currentStep">
+ ✓
+ currentStep">{{ i + 1 }}
+
+
{{ step }}
+
+
+
+
+
+
+
+
+
+
Your Items
+
+
0; else noOrder">
+
+
![{{ item.name }}]()
+
+
+
{{ item.name }}
+
+ ${{ (item.price * item.quantity).toFixed(2) }}
+
+
+
{{ item.description }}
+
+ Qty: {{ item.quantity }}
+ Unit Price: ${{ item.price.toFixed(2) }}
+
+
+ {{ item.status }}
+
+
+
+
+
+
+
+
+
0" class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 sticky top-6">
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0; else noOrders">
+
+
+
+
Order ID
+
Date
+
Items
+
Total
+
Status
+
Action
+
+
+
+
+
+
+
+
+
+
Order #{{ order.id }}
+
{{ formatDate(order.date) }}
+
+
+ {{ order.status }}
+
+
+
Items: {{ order.itemsCount }}
+
Total: ${{ order.total.toFixed(2) }}
+
+
+
+
+
+
+
+
+
#{{ order.id }}
+
{{ formatDate(order.date) }}
+
{{ order.itemsCount }}
+
${{ order.total.toFixed(2) }}
+
+
+ {{ order.status }}
+
+
+
+
+ Details
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+ }
+
+
\ 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; }