diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e36d0ad..247be02 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,6 +1,7 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { routes } from './app.routes'; @@ -9,6 +10,7 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimations(), + provideHttpClient(withInterceptorsFromDi()), ] }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 24b2764..ca92e76 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -63,13 +63,24 @@ export const routes: Routes = [ component: ForecastComponent, pathMatch: 'full', }, - { path: 'order-history', component: OrderHistoryComponent }, // /dashboard/orders - { path: 'inventory', component: InventoryComponent }, // /dashboard/inventory - { path: 'product-management', component: ProductManagementComponent }, // /dashboard/product-management - { path: 'current-requests', component: CurrentRequestsComponent }, // /dashboard/current-requests - // { path: 'deliveries', component: DeliveriesComponent }, // /dashboard/deliveries - // { path: 'vendors', component: VendorsComponent }, - { path: 'supplier', component: SupplierDashboard, pathMatch: 'full' }, + + // Supplier Routes + { + path: 'supplier', + // canActivate: [roleGuard(3)], + children: [ + { path: '', redirectTo: 'profile', pathMatch: 'full' }, + { path: 'order-history', component: OrderHistoryComponent }, + { path: 'inventory', component: InventoryComponent }, + { path: 'product-management', component: ProductManagementComponent }, + { path: 'current-requests', component: CurrentRequestsComponent }, + // { path: 'deliveries', component: DeliveriesComponent }, + // { path: 'vendors', component: VendorsComponent }, + { path: 'supplier', component: SupplierDashboard, pathMatch: 'full' }, + { path: 'profile', component: ProfileComponent }, + { path: '**', redirectTo: '', pathMatch: 'full' }, + ], + }, ], }, { path: '**', redirectTo: 'home' }, // Handle 404/unknown routes diff --git a/src/app/components/sidebar/sidebar.component.ts b/src/app/components/sidebar/sidebar.component.ts index 30079e0..90926c2 100644 --- a/src/app/components/sidebar/sidebar.component.ts +++ b/src/app/components/sidebar/sidebar.component.ts @@ -24,57 +24,17 @@ export class SidebarComponent { // d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />`) }, { - label: 'Forecast', - route: 'forecast', - // icon: this.sanitize(``) - }, - { - label: 'Supplier', - route: 'supplier', - // icon: this.sanitize(``) - }, - { - label: 'Orders', - route: '/orders', - // icon: this.sanitize(``) + label: 'Product Managment', + route: '/dashboard/supplier/product-management', }, { - label: 'Inventory', - route: '/inventory', - // icon: this.sanitize(``) + label: 'Inventory Managment', + route: '/dashboard/supplier/inventory', }, { - label: 'Deliveries', - route: '/deliveries', - // icon: this.sanitize(``) - }, - { - label: 'Warehouse-Inventory', - route: '/dashboard/warehouse', - }, - { - label: 'Warehouse-Truck Tracking', - route: '/dashboard/warehouse/truck-tracking', - }, - { - label: 'Warehouse-Vendor Orders', - route: '/dashboard/warehouse/vendor-orders', - }, - { - label: 'Warehouse-Supplier Requests', - route: '/dashboard/warehouse/supplier-requests', - } - ];; + ]; isClosed = input(false); } diff --git a/src/app/components/supplier-page/current-requests/current-requests.component.html b/src/app/components/supplier-page/current-requests/current-requests.component.html index 0218bf1..a40f986 100644 --- a/src/app/components/supplier-page/current-requests/current-requests.component.html +++ b/src/app/components/supplier-page/current-requests/current-requests.component.html @@ -1,59 +1,387 @@ -
-

Current Requests & Deadlines

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Request ID (PO#)Request DateProductQuantity RequestedRequired Delivery DateStatusActions
{{ request.id }}{{ request.requestDate | date:'shortDate' }}{{ request.productName }}{{ request.quantity }}{{ request.deadline | date:'shortDate' }} - - {{ request.status }} - - - - - - - - -
No current requests found.
+ +
+ +
+
+
+

+ Order Management +

+

+ Current Requests & History +

+

+ Manage incoming orders and view your delivery history +

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

Pending Orders

+

{{ pendingCount }}

+
+
+

+ Accepted Orders +

+

{{ acceptedCount }}

+
+
+

+ Delivered Orders +

+

{{ receivedCount }}

+
+
+ + + +
+

Loading requests...

+
+
+ + + + + +
+

+ + + + Active Orders +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Request IDRequest DateProductQuantityDelivery DeadlineStatusActions
+ {{ request.id }} + + {{ request.requestDate | date : "shortDate" }} + {{ request.productName }}{{ request.quantity | number }} + {{ request.deadline | date : "shortDate" }} + + + {{ request.status }} + + + + + + + +
+ No active orders found. +
+
+
+ + +
+

+ + + + Order History +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Request IDRequest DateProductQuantityOutcome DateStatusActions
+ {{ request.id }} + + {{ request.requestDate | date : "shortDate" }} + {{ request.productName }}{{ request.quantity | number }} + + + {{ request.receivedAt | date : "shortDate" }} (Received) + + + + {{ request.deadline | date : "shortDate" }} (Expected) + + + N/A + + + + {{ request.status }} + + + +
+ No historical orders found. +
+
- - - -
-
- © 2025 Supply Chain Management System. All rights reserved. + +
+ + +
+ +
+
+

+ Request Details (ID: {{ selectedRequestDetails.id }}) +

+ +
+
+
+
+ Product: {{ selectedRequestDetails.productName }}
-
\ No newline at end of file +
+ Quantity: + {{ selectedRequestDetails.quantity | number }} +
+
+ Request Date: + {{ selectedRequestDetails.requestDate | date : "medium" }} +
+
+ Delivery Deadline: + {{ selectedRequestDetails.deadline | date : "medium" }} +
+
+ Current Status: + + {{ selectedRequestDetails.status }} + +
+ +
Supplier Name: {{ raw.supplier_name }}
+
Supplier ID: {{ raw.supplier_id }}
+
Product ID: {{ raw.product_id }}
+
Warehouse ID: {{ raw.warehouse_id }}
+
+ Unit Price: + {{ raw.unit_price | currency : "USD" : "symbol" : "1.2-2" }} +
+
+ Received At: + {{ raw.received_at | date : "medium" }} +
+ +
+ Quality: {{ raw.quality }} +
+
+ Is Defective: {{ raw.is_defective ? "Yes" : "No" }} +
+
+
+
{{ selectedRequestDetails.rawApiData | json }}
+
+
+ +
+
+
+ + +
+
+ © 2025 Supply Chain Management System. All rights reserved. +
+
diff --git a/src/app/components/supplier-page/current-requests/current-requests.component.ts b/src/app/components/supplier-page/current-requests/current-requests.component.ts index dcc97fa..e3ae3b4 100644 --- a/src/app/components/supplier-page/current-requests/current-requests.component.ts +++ b/src/app/components/supplier-page/current-requests/current-requests.component.ts @@ -1,131 +1,263 @@ import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; // <--- IMPORT THIS +import { CommonModule, DatePipe } from '@angular/common'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { jwtDecode } from 'jwt-decode'; -// Interface to define the structure of a request item +// Interface for the data structure from your API +interface ApiRequestItem { + request_id: number; + supplier_id: number; + created_at: string; + expected_delivery_date: string; + product_id: number; + count: number; + status: 'pending' | 'accepted' | 'rejected' | 'received' | 'returned'; + received_at: string | null; + warehouse_id: number; + unit_price: number | null; + quality: string | null; + is_defective: boolean | null; + supplier_name: string; + product_name: string; +} + +// Your internal interface for display and component logic interface RequestItem { - id: string; - requestDate: Date | string; + id: number; + requestDate: Date; productName: string; quantity: number; - deadline: Date | string; - status: 'New' | 'Accepted' | 'Rejected' | 'Shipped' | 'Processing'; + deadline: Date; + status: 'Pending' | 'Accepted' | 'Rejected' | 'Received' | 'Returned'; + receivedAt: Date | null; // Added for "Delivered Date" + rawApiData?: ApiRequestItem; + supplierId?: number; + productId?: number; + warehouseId?: number; + unitPrice?: number | null; + quality?: string | null; + isDefective?: boolean | null; + supplierName?: string; +} + +interface JWTPayload { + user_id: number; + username: string; + role_id: number; } @Component({ selector: 'app-current-requests', - standalone: true, // <--- MAKE SURE THIS IS TRUE - imports: [CommonModule], // <--- ADD THIS IMPORTS ARRAY WITH CommonModule + standalone: true, + imports: [CommonModule], templateUrl: './current-requests.component.html', - styleUrls: ['./current-requests.component.css'] + styleUrls: ['./current-requests.component.css'], + providers: [DatePipe] }) export class CurrentRequestsComponent implements OnInit { + // Master list from API + // allRequests: RequestItem[] = []; // We can derive filtered lists directly + + // Filtered lists for tables + activeRequests: RequestItem[] = []; + otherRequests: RequestItem[] = []; // For Rejected, Returned, Received - currentRequests: RequestItem[] = []; + // Counts for summary cards + pendingCount: number = 0; + acceptedCount: number = 0; + receivedCount: number = 0; // For 'Received' status orders - constructor() { } + isLoading: boolean = false; + errorMessage: string | null = null; + selectedRequestDetails: RequestItem | null = null; + isModalOpen: boolean = false; + + private supplierId!: number; // Will be set in ngOnInit + + private readonly ORDER_MANAGEMENT_API_BASE_URL = 'http://localhost:8000/api/v0/supplier-request'; + + constructor(private http: HttpClient, public datePipe: DatePipe) {} ngOnInit(): void { - this.loadHardcodedRequests(); + let decoded: JWTPayload | null = null; + const token = localStorage.getItem("token"); + if (token) { + try { + decoded = jwtDecode(token); + console.log('Decoded token:', decoded); + this.supplierId = decoded?.user_id || 1; // Set supplierId here + } catch (error) { + console.error('Error decoding token:', error); + this.supplierId = 1; // Fallback supplierId + } + } else { + this.supplierId = 1; // Fallback if no token + console.warn('No token found, using fallback supplierId.'); + } + this.loadRequests(); // No need to pass supplierId if it's a class member } - loadHardcodedRequests(): void { - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); - const tomorrow = new Date(today); - tomorrow.setDate(today.getDate() + 1); - const nextWeek = new Date(today); - nextWeek.setDate(today.getDate() + 7); - - // Ensure dates are consistently Dates or consistently strings if needed - // Using Date objects is generally better for date logic - this.currentRequests = [ - { - id: 'PO-1001', - requestDate: yesterday, // Keep as Date object - productName: 'Cumin Seeds', - quantity: 50, - deadline: tomorrow, // Keep as Date object - status: 'New' - }, - { - id: 'PO-1002', - requestDate: new Date(new Date().setDate(new Date().getDate() - 5)), // Use new Date() to avoid modifying 'today' - productName: 'Cloves', - quantity: 10, - deadline: nextWeek, // Keep as Date object - status: 'Accepted' - }, - { - id: 'PO-1003', - requestDate: new Date(new Date().setDate(new Date().getDate() - 10)), - productName: 'Cinnamon Sticks', - quantity: 200, - deadline: yesterday, // Re-use yesterday variable if it's correct scope, or recalculate - status: 'Processing' - }, - { - id: 'PO-1004', - requestDate: new Date(new Date().setDate(new Date().getDate() - 2)), - productName: 'Garam Masala', - quantity: 5, - deadline: new Date(new Date().setDate(new Date().getDate() + 3)), - status: 'New' - }, - { - id: 'PO-1005', - requestDate: new Date(new Date().setDate(new Date().getDate() - 15)), - productName: 'Chili Powder', - quantity: 25, - deadline: new Date(new Date().setDate(new Date().getDate() - 8)), - status: 'Accepted' + loadRequests(): void { + this.isLoading = true; + this.errorMessage = null; + // Reset lists and counts + this.activeRequests = []; + this.otherRequests = []; + this.pendingCount = 0; + this.acceptedCount = 0; + this.receivedCount = 0; + + if (!this.supplierId) { + this.errorMessage = "Supplier ID not available. Cannot load requests."; + this.isLoading = false; + return; + } + + this.http.get(`${this.ORDER_MANAGEMENT_API_BASE_URL}/supplier/${this.supplierId}/`).pipe( + map(apiDataArray => { + // First, transform all API items + return apiDataArray.map(apiItem => this.transformApiItemToRequestItem(apiItem)); + }), + tap(transformedRequests => { + // Second, categorize and count + this.processAndCategorizeRequests(transformedRequests); + }), + catchError((error: HttpErrorResponse) => { + console.error('Error fetching requests:', error); + this.errorMessage = `Failed to load requests: ${error.statusText || 'Unknown error'}`; + return of([]); // Return empty on error so tap doesn't break if array is expected + }) + ).subscribe({ + // Data processing is now in 'tap', 'next' can be minimal or for final checks + next: () => { + this.isLoading = false; }, - { - id: 'PO-1006', - requestDate: new Date(new Date().setDate(new Date().getDate() - 3)), - productName: 'Cardamom Pods', - quantity: 30, - deadline: new Date(new Date().setDate(new Date().getDate() + 10)), - status: 'Rejected' + error: () => { + // Error is caught by catchError, this ensures isLoading is false + this.isLoading = false; + } + }); + } + + private processAndCategorizeRequests(requests: RequestItem[]): void { + this.activeRequests = []; + this.otherRequests = []; + this.pendingCount = 0; + this.acceptedCount = 0; + this.receivedCount = 0; + + requests.forEach(request => { + if (request.status === 'Pending' || request.status === 'Accepted') { + this.activeRequests.push(request); + } else if (request.status === 'Rejected' || request.status === 'Returned' || request.status === 'Received') { + this.otherRequests.push(request); } - ]; - // It's better to recalculate dates if modifying them in loops/assignments + + // Update counts + if (request.status === 'Pending') this.pendingCount++; + if (request.status === 'Accepted') this.acceptedCount++; + if (request.status === 'Received') this.receivedCount++; + }); + } + + + private transformApiItemToRequestItem(apiItem: ApiRequestItem): RequestItem { + const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); + return { + id: apiItem.request_id, + requestDate: new Date(apiItem.created_at), + productName: apiItem.product_name, + quantity: apiItem.count, + deadline: new Date(apiItem.expected_delivery_date), + status: capitalize(apiItem.status) as RequestItem['status'], + receivedAt: apiItem.received_at ? new Date(apiItem.received_at) : null, // Map received_at + rawApiData: apiItem, + supplierId: apiItem.supplier_id, + productId: apiItem.product_id, + warehouseId: apiItem.warehouse_id, + unitPrice: apiItem.unit_price, + quality: apiItem.quality, + isDefective: apiItem.is_defective, + supplierName: apiItem.supplier_name + }; } - isOverdue(deadline: Date | string): boolean { + isOverdue(deadline: Date): boolean { const today = new Date(); today.setHours(0, 0, 0, 0); - const deadlineDate = new Date(deadline); // Convert string or Date object to Date + const deadlineDate = new Date(deadline); deadlineDate.setHours(0, 0, 0, 0); - return deadlineDate < today; + return deadlineDate < today && !this.isToday(deadlineDate); } - getRequestStatusClass(status: string): string { + isToday(date: Date): boolean { + const today = new Date(); + return date.getFullYear() === today.getFullYear() && + date.getMonth() === today.getMonth() && + date.getDate() === today.getDate(); + } + + getRequestStatusClass(status: RequestItem['status']): string { switch (status) { - case 'New': return 'bg-blue-100 text-blue-700'; + case 'Pending': return 'bg-blue-100 text-blue-700'; case 'Accepted': return 'bg-green-100 text-green-700'; - case 'Processing': return 'bg-yellow-100 text-yellow-700'; - case 'Shipped': return 'bg-purple-100 text-purple-700'; case 'Rejected': return 'bg-red-100 text-red-700'; + case 'Received': return 'bg-purple-100 text-purple-700'; + case 'Returned': return 'bg-orange-100 text-orange-700'; default: return 'bg-gray-100 text-gray-700'; } } - acceptRequest(requestId: string): void { + updateRequestStatus(requestId: number, newApiStatus: 'accepted' | 'rejected'): void { + this.isLoading = true; + const updateUrl = `${this.ORDER_MANAGEMENT_API_BASE_URL}/${requestId}/status/`; + + this.http.patch(updateUrl, { status: newApiStatus }) + .pipe( + tap(() => { + console.log(`Request ${requestId} status update to ${newApiStatus} successful.`); + }), + catchError((error: HttpErrorResponse) => { + console.error(`Error updating request ${requestId}:`, error); + this.errorMessage = `Failed to update request ${requestId}. ${error.message || 'Please try again.'}`; + // Do not set isLoading false here if switchMap follows, let loadRequests handle it + return throwError(() => error); + }), + switchMap(() => { + this.loadRequests(); // Reload all requests, which will re-filter and update counts + return of(null); + }) + ) + .subscribe({ + error: () => { + // If switchMap or loadRequests itself errors out and doesn't complete + this.isLoading = false; + }, + complete: () => { + // isLoading is typically set to false at the end of loadRequests + } + }); + } + + acceptRequest(requestId: number): void { console.log(`Accepting request: ${requestId}`); - const request = this.currentRequests.find(r => r.id === requestId); - if (request) { - request.status = 'Accepted'; - } - // TODO: Integrate with backend service and blockchain trigger + this.updateRequestStatus(requestId, 'accepted'); } - rejectRequest(requestId: string): void { + rejectRequest(requestId: number): void { console.log(`Rejecting request: ${requestId}`); - const request = this.currentRequests.find(r => r.id === requestId); - if (request) { - request.status = 'Rejected'; - } - // TODO: Integrate with backend service and blockchain trigger + this.updateRequestStatus(requestId, 'rejected'); + } + + openDetailsModal(request: RequestItem): void { + this.selectedRequestDetails = request; + this.isModalOpen = true; + } + + closeDetailsModal(): void { + this.isModalOpen = false; + this.selectedRequestDetails = null; } } \ No newline at end of file diff --git a/src/app/components/supplier-page/inventory/inventory.component.css b/src/app/components/supplier-page/inventory/inventory.component.css index e69de29..b310323 100644 --- a/src/app/components/supplier-page/inventory/inventory.component.css +++ b/src/app/components/supplier-page/inventory/inventory.component.css @@ -0,0 +1,11 @@ +.transactions-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.header p { + color: #666; + font-size: 16px; +} \ No newline at end of file diff --git a/src/app/components/supplier-page/inventory/inventory.component.html b/src/app/components/supplier-page/inventory/inventory.component.html index cd48158..a9c539e 100644 --- a/src/app/components/supplier-page/inventory/inventory.component.html +++ b/src/app/components/supplier-page/inventory/inventory.component.html @@ -1,7 +1,43 @@ - -
-

Current Inventory

- +
+ + +
+
+
+

+ Stock Management +

+

+ Current Inventory +

+

+ Check current stock levels across warehouses +

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

Loading inventory...

+ +
+
+ + + + +
@@ -37,4 +73,15 @@

Current Inventory

-
\ No newline at end of file + +
+

No inventory data available.

+
+
+
+ +
+
+ © 2025 Supply Chain Management System. All rights reserved. +
+
\ No newline at end of file diff --git a/src/app/components/supplier-page/inventory/inventory.component.ts b/src/app/components/supplier-page/inventory/inventory.component.ts index 8638833..29a5962 100644 --- a/src/app/components/supplier-page/inventory/inventory.component.ts +++ b/src/app/components/supplier-page/inventory/inventory.component.ts @@ -1,14 +1,23 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule, NgClass } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; // Import HttpClient +import { Observable, of } from 'rxjs'; // Import Observable and of for error handling +import { catchError, map } from 'rxjs/operators'; // Import operators + +// Interface for the data structure coming from your API +interface ApiInventoryItem { + product_name: string; + SKU: string; + warehouse: string; + Quantity_on_hand: number; +} -// Optional: Define an interface for better type safety interface InventoryItem { productName: string; sku: string; warehouseLocation: string; quantity: number; - status: string; // Or a specific type like 'Available' | 'Low Stock' - // Add any other properties your items have + status: 'Available' | 'Low Stock' | 'Out of Stock'; // Specific status types } @Component({ @@ -19,39 +28,65 @@ interface InventoryItem { styleUrl: './inventory.component.css' }) -// Implement OnInit if you fetch data when the component loads export class InventoryComponent implements OnInit { - // --- ADD THIS LINE --- - // Initialize as an empty array. Use 'any[]' for now, or the interface inventoryItems: InventoryItem[] = []; - // OR if you don't want strict typing yet: - // inventoryItems: any[] = []; + isLoading: boolean = false; // To show a loading indicator + errorMessage: string | null = null; // To display any API errors + + // API URL - + // In a real app, the base URL and supplier_id might come from environment variables or a service + private apiUrl = 'http://localhost:8000/api/warehouse/supplier-dashboard/?supplier_id=103'; - // Constructor (Inject services here if needed) - constructor(/* private inventoryService: InventoryService */) { } + constructor(private http: HttpClient) { } // Inject HttpClient - // ngOnInit is a good place to fetch initial data ngOnInit(): void { - this.loadInventory(); // Call a method to load data + this.loadInventory(); } loadInventory(): void { - // Here you would typically call a service to get the data - // For example: - // this.inventoryService.getInventory().subscribe(data => { - // this.inventoryItems = data; - // }); - - // --- TEMPORARY Placeholder Data (Remove when using a service) --- - this.inventoryItems = [ - { productName: 'Turmeric Powder', sku: 'LP123', warehouseLocation: 'Main WH', quantity: 50, status: 'Available' }, - { productName: 'Garam Masala', sku: 'WM456', warehouseLocation: 'Main WH', quantity: 5, status: 'Low Stock' }, - { productName: 'Cinnamon Sticks', sku: 'KB789', warehouseLocation: 'Secondary WH', quantity: 0, status: 'Out of Stock' } - ]; - // --- END Placeholder --- - } + this.isLoading = true; + this.errorMessage = null; + this.inventoryItems = []; // Clear previous items + + this.http.get(this.apiUrl).pipe( + map(apiData => { + // Transform API data to our InventoryItem structure and calculate status + return apiData.map(apiItem => { + let currentStatus: 'Available' | 'Low Stock' | 'Out of Stock'; + const quantity = apiItem.Quantity_on_hand; - // ... other methods if needed + if (quantity === 0) { + currentStatus = 'Out of Stock'; + } else if (quantity > 0 && quantity <= 150000) { // Between 0 (exclusive) and 150000 (inclusive) + currentStatus = 'Low Stock'; + } else { // quantity > 150000 + currentStatus = 'Available'; + } + return { + productName: apiItem.product_name, + sku: apiItem.SKU, + warehouseLocation: apiItem.warehouse, + quantity: quantity, + status: currentStatus + }; + }); + }), + catchError(error => { + console.error('Error fetching inventory data:', error); + this.errorMessage = 'Failed to load inventory data. Please try again later.'; + return of([]); // Return an empty array on error to prevent breaking the UI + }) + ).subscribe({ + next: (processedData) => { + this.inventoryItems = processedData; + this.isLoading = false; + }, + error: () => { + // Error handling is already done in catchError, but you can add more here if needed + this.isLoading = false; + } + }); + } } diff --git a/src/app/components/supplier-page/product-management/product-management.component.html b/src/app/components/supplier-page/product-management/product-management.component.html index b47981b..4d57a3d 100644 --- a/src/app/components/supplier-page/product-management/product-management.component.html +++ b/src/app/components/supplier-page/product-management/product-management.component.html @@ -1,88 +1,207 @@ - -
-

Manage Products

- - + +
+ +
+
+
+

Supply Management

+

Product Catalogue

+

View your products and manage their prices per warehouse.

+
+
+
+ + + +
+
+ +
+
-
- - -
-

{{ editingProduct ? 'Edit Product' : 'Add New Product' }}

- -
-
-
- - - -
-
- - -
-
- - -
-
- - -
- -
-
- - -
-
- -
- {{ successMessage }} -
-
- {{ errorMessage }} -
+ + +
+ +

Loading product catalogue...

+
+
+ {{ successMessage }}
- - +
+ {{ errorMessage }} +
+ -

Your Product Catalogue

-
- - - - - - - - - - - - - - - - - - - - -
Product NameSKUUnit PriceActions
{{ product.name }}{{ product.sku }}{{ product.price | currency }} - - -
You haven't added any products yet.
+

Your Product Catalogue

+
+
+ + + + + + + + + + + + + + + + + + + + + + +
Product NameSKUGeneral PricePriced WarehousesActions
{{ product.name }}{{ product.sku }} {{ product.price | currency:'USD':'symbol':'1.2-2' }} + + + {{ wh }} + + + Not priced in any warehouse + + +
+
+ No products found in your catalogue. +
+
- -
-
- © 2025 Supply Chain Management System. All rights reserved. +
+ + + +
+
+
+

Add Product to Warehouse

+
+
+
+ + +
Product selection is required.
+
+ +
+ + +
+ +
+ + +
Warehouse selection is required.
+
+ +
+ + +
+ Price is required. + Price must be 0 or more. +
+
+ +
+ + +
+
+
+
+ + + +
+
+
+

Set/Update Price for Warehouse

+ +
+
+
+

Product: {{ productForPriceEdit.name }}

+

SKU: {{ productForPriceEdit.sku }}

+
+
+ + +
Warehouse selection is required.
+
+
+ + +
+ Price is required. + Price must be 0 or more. +
+
+
+ + +
+
+
+ + +
+ © 2025 Supply Chain Management System. All rights reserved. +
\ No newline at end of file diff --git a/src/app/components/supplier-page/product-management/product-management.component.ts b/src/app/components/supplier-page/product-management/product-management.component.ts index 1da4800..2e97e56 100644 --- a/src/app/components/supplier-page/product-management/product-management.component.ts +++ b/src/app/components/supplier-page/product-management/product-management.component.ts @@ -1,16 +1,39 @@ -import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; // Likely needed for add/edit form -import { CommonModule } from '@angular/common'; // Ensure this is imported if standalone or in NgModule -import { ReactiveFormsModule } from '@angular/forms'; // Import if using reactive forms +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { CommonModule, CurrencyPipe } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { of, Subject, throwError } from 'rxjs'; +import { catchError, map, takeUntil, tap } from 'rxjs/operators'; +// Interface for the data from your LISTING API +interface ApiProductListItem { + product_name: string; + SKU: string; + supplier_price: number; + warehouses: string[]; // Names of warehouses +} -interface Product { - id: string | number; // Use appropriate type for your ID +// Interface for your component's product structure (for display in table) +interface ProductDisplay { name: string; sku: string; - description?: string; // Optional property price: number; - // Add other product properties + warehouses: string[]; // Names of warehouses this product is in + description?: string; +} + +// Interface for warehouse selection (with ID) +interface Warehouse { + id: number; + name: string; +} + +// Interface for predefined product selection +interface PredefinedProduct { + name: string; + id: number; // The actual product_id + sku: string; // The generated SKU } @Component({ @@ -18,110 +41,273 @@ interface Product { selector: 'app-product-management', imports: [CommonModule, ReactiveFormsModule], templateUrl: './product-management.component.html', - styleUrls: ['./product-management.component.css'] + styleUrls: ['./product-management.component.css'], + providers: [CurrencyPipe] }) +export class ProductManagementComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); -export class ProductManagementComponent implements OnInit { + products: ProductDisplay[] = []; + + isAddProductToWarehouseModalOpen: boolean = false; + addProductToWarehouseForm!: FormGroup; + predefinedProductsForDropdown: PredefinedProduct[] = []; + + editPriceForm!: FormGroup; + isEditPriceModalOpen: boolean = false; + productForPriceEdit: ProductDisplay | null = null; + filteredWarehousesForModal: Warehouse[] = []; + + readonly allAvailableWarehouses: Warehouse[] = [ + { id: 1, name: 'Colombo Central' }, + { id: 2, name: 'Kandy Depot' }, + { id: 3, name: 'Kurunegala Rock' } + ]; - // Properties for the component - products: Product[] = []; - productForm!: FormGroup; // For Add/Edit - showAddForm: boolean = false; - editingProduct: Product | null = null; successMessage: string | null = null; errorMessage: string | null = null; + isLoading: boolean = false; + isSubmitting: boolean = false; + + private readonly supplierId: number = 103; + private readonly PRODUCT_LIST_API_URL = 'http://127.0.0.1:8000/api/warehouse/supplier-product/prices/'; + // Assuming Django URL is fixed to use a single slash: + private readonly ADD_OR_UPDATE_PRICE_API_URL = 'http://127.0.0.1:8000/api/warehouse//supplier-product/add_or_update/'; - // Inject FormBuilder and your ProductService constructor( - private fb: FormBuilder /*, - private productService: ProductService */ // Uncomment when service is ready - ) { } + private fb: FormBuilder, + private http: HttpClient + ) {} ngOnInit(): void { + this.generatePredefinedProducts(); + this.initAddProductToWarehouseForm(); + this.initEditPriceForm(); this.loadProducts(); - this.initForm(); } - initForm(): void { - this.productForm = this.fb.group({ - id: [null], // Keep track of id for editing - name: ['', Validators.required], - sku: ['', Validators.required], - description: [''], - price: [0, [Validators.required, Validators.min(0)]] - }); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } - loadProducts(): void { - // TODO: Replace with actual service call - // this.productService.getProducts().subscribe(data => this.products = data); - console.log('Loading products...'); - // Placeholder: - this.products = [ - { id: 'p1', name: 'Sample Item 1', sku: 'SKU001', price: 19.99 }, - { id: 'p2', name: 'Sample Item 2', sku: 'SKU002', price: 29.99, description: 'A second item' } + generatePredefinedProducts(): void { + const productNames = [ + 'Chili Product 1', 'Cinnamon Product 2', 'Pepper Product 3', 'Cinnamon Product 4', + 'Chili Product 5', 'Cinnamon Product 6', 'Pepper Product 7', 'Pepper Product 8', + 'Chili Product 9', 'Cinnamon Product 10' ]; + this.predefinedProductsForDropdown = productNames.map(name => { + const id = this.extractProductIdFromName(name); + return { + name: name, + id: id || 0, + sku: this.generateSku(id) // SKU is generated here + }; + }); } - onSubmit(): void { - // Logic for saving/updating product using this.productForm.value - console.log('Form submitted:', this.productForm.value); - this.successMessage = 'Product saved successfully!'; // Example message - // Reset form, reload products etc. - this.resetForm(); - this.loadProducts(); // Reload list after save/update + initAddProductToWarehouseForm(): void { + this.addProductToWarehouseForm = this.fb.group({ + selected_product_id: [null, Validators.required], + sku_display: [{ value: '', disabled: true }], // Disabled input for SKU + warehouse_id: [null, Validators.required], + supplier_price: [null, [Validators.required, Validators.min(0)]] + }); + + // Auto-update SKU when product changes + this.addProductToWarehouseForm.get('selected_product_id')?.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(productId => { + const selectedProduct = this.predefinedProductsForDropdown.find(p => p.id === productId); + this.addProductToWarehouseForm.get('sku_display')?.setValue(selectedProduct ? selectedProduct.sku : ''); + }); } - editProduct(product: Product): void { - this.editingProduct = product; - this.productForm.patchValue(product); // Load product data into form - this.showAddForm = true; // Show the form - this.successMessage = null; - this.errorMessage = null; + initEditPriceForm(): void { + this.editPriceForm = this.fb.group({ + warehouse_id: [null, Validators.required], + supplier_price: [null, [Validators.required, Validators.min(0)]] + }); } - cancelEdit(): void { - this.resetForm(); + // Helper to generate SKU "SKUXXX" + generateSku(productId: number | null): string { + if (productId === null || productId === 0) return 'N/A'; // Or some placeholder + return `SKU${String(productId).padStart(3, '0')}`; } - resetForm(): void { - this.editingProduct = null; - this.productForm.reset(); - this.showAddForm = false; - this.successMessage = null; + // Helper to extract product ID from name "Product Name X" + extractProductIdFromName(productName: string): number | null { + const match = productName.match(/\s(\d+)$/); + if (match && match[1]) { + return parseInt(match[1], 10); + } + console.warn(`Could not extract product ID from name: ${productName}`); + return null; + } + + loadProducts(): void { + this.isLoading = true; this.errorMessage = null; + this.products = []; + const apiUrl = `${this.PRODUCT_LIST_API_URL}?supplier_id=${this.supplierId}`; + + this.http.get(apiUrl).pipe( + map(apiDataArray => + apiDataArray.map(apiItem => ({ + name: apiItem.product_name, + sku: apiItem.SKU, // This SKU comes from the API for existing products + price: apiItem.supplier_price, + warehouses: apiItem.warehouses || [] + })) + ), + catchError((error: HttpErrorResponse) => { + console.error('Error fetching products:', error); + this.errorMessage = `Failed to load product catalogue: ${error.statusText || 'Unknown error'}`; + return of([]); + }) + ).subscribe({ + next: (transformedProducts) => { + this.products = transformedProducts; + this.isLoading = false; + }, + error: () => { this.isLoading = false; } + }); + } + + openAddProductToWarehouseModal(): void { + this.isAddProductToWarehouseModalOpen = true; + this.addProductToWarehouseForm.reset(); + this.addProductToWarehouseForm.get('sku_display')?.setValue(''); + this.clearMessages(); } - // --- ADD THIS METHOD --- - deleteProduct(productId: string | number): void { - // Optional: Add a confirmation dialog - if (confirm(`Are you sure you want to delete product with ID: ${productId}?`)) { - console.log('Attempting to delete product with ID:', productId); - - // TODO: Replace with actual service call - // this.productService.deleteProduct(productId).subscribe({ - // next: () => { - // console.log('Product deleted successfully'); - // this.successMessage = 'Product deleted successfully!'; - // // Remove the product from the local list to update UI instantly - // this.products = this.products.filter(p => p.id !== productId); - // this.errorMessage = null; - // }, - // error: (err) => { - // console.error('Error deleting product:', err); - // this.errorMessage = 'Failed to delete product. Please try again.'; - // this.successMessage = null; - // } - // }); - - // --- Placeholder for testing without service --- - this.products = this.products.filter(p => p.id !== productId); - this.successMessage = `Placeholder: Product ${productId} deleted.`; - this.errorMessage = null; - console.log(`Placeholder: Deleted product ${productId}`); - // --- End Placeholder --- + closeAddProductToWarehouseModal(): void { + this.isAddProductToWarehouseModalOpen = false; + this.addProductToWarehouseForm.reset(); + } + + onAddProductToWarehouseSubmit(): void { + if (this.addProductToWarehouseForm.invalid) { + this.showGeneralFormError("Please select a product, warehouse, and enter a valid price."); + return; + } + this.isSubmitting = true; + this.clearMessages(); + + const formValue = this.addProductToWarehouseForm.value; // Use getRawValue() if you need disabled field values + const payload = { + warehouse_id: Number(formValue.warehouse_id), + supplier_id: this.supplierId, + product_id: Number(formValue.selected_product_id), + supplier_price: Number(formValue.supplier_price) + // The SKU is not sent in this payload because the backend likely identifies the product by product_id. + // The SKU generation is primarily for display consistency in the UI. + }; + + console.log('Adding product to warehouse with payload:', payload); + const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; + + this.http.post(this.ADD_OR_UPDATE_PRICE_API_URL, payload, httpOptions).pipe( + catchError((error: HttpErrorResponse) => { + console.error('Error adding product to warehouse:', error); + let detail = error.error && typeof error.error === 'object' ? JSON.stringify(error.error) : (error.error || error.message); + this.showGeneralFormError(`Failed to add product: ${error.statusText || 'Unknown error'}. ${detail}`); + this.isSubmitting = false; + return throwError(() => error); + }) + ).subscribe({ + next: (response) => { + const selectedProduct = this.predefinedProductsForDropdown.find(p => p.id === payload.product_id); + this.showGeneralFormSuccess(`Product "${selectedProduct?.name || 'Selected Product'}" successfully added/updated for the warehouse.`); + this.isSubmitting = false; + this.closeAddProductToWarehouseModal(); + this.loadProducts(); + }, + error: () => { this.isSubmitting = false; } + }); + } + + openEditPriceModal(product: ProductDisplay): void { + this.productForPriceEdit = product; + this.editPriceForm.reset(); + this.clearMessages(); + + if (product.warehouses && product.warehouses.length > 0) { + this.filteredWarehousesForModal = this.allAvailableWarehouses.filter(wh => + product.warehouses.includes(wh.name) + ); + } else { + this.filteredWarehousesForModal = []; + } + if (this.filteredWarehousesForModal.length === 1) { + this.editPriceForm.patchValue({ warehouse_id: this.filteredWarehousesForModal[0].id }); + } + this.isEditPriceModalOpen = true; + } + + closeEditPriceModal(): void { + this.isEditPriceModalOpen = false; + this.productForPriceEdit = null; + this.filteredWarehousesForModal = []; + this.editPriceForm.reset(); + } + + onUpdatePriceSubmit(): void { + if (!this.productForPriceEdit || this.editPriceForm.invalid) { + this.showGeneralFormError("Modal Error: Please select a warehouse and enter a valid price."); + return; + } + this.isSubmitting = true; + this.clearMessages(); + + const productId = this.extractProductIdFromName(this.productForPriceEdit.name); + if (productId === null) { + this.showGeneralFormError(`Modal Error: Could not determine Product ID for "${this.productForPriceEdit.name}".`); + this.isSubmitting = false; + return; } + + const formValue = this.editPriceForm.value; + const payload = { + warehouse_id: Number(formValue.warehouse_id), + supplier_id: this.supplierId, + product_id: productId, + supplier_price: Number(formValue.supplier_price) + }; + console.log('Updating price with payload:', payload); + const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; + + this.http.post(this.ADD_OR_UPDATE_PRICE_API_URL, payload, httpOptions).pipe( + catchError((error: HttpErrorResponse) => { + console.error('Error updating price:', error); + let detail = error.error && typeof error.error === 'object' ? JSON.stringify(error.error) : (error.error || error.message); + this.showGeneralFormError(`Modal Error: Failed to update price: ${error.statusText}. ${detail}`); + this.isSubmitting = false; + return throwError(() => error); + }) + ).subscribe({ + next: (response) => { + this.showGeneralFormSuccess(`Price for "${this.productForPriceEdit?.name}" updated successfully.`); + this.isSubmitting = false; + this.closeEditPriceModal(); + this.loadProducts(); + }, + error: () => { this.isSubmitting = false; } + }); } - // --- END OF METHOD --- + clearMessages(): void { + this.successMessage = null; + this.errorMessage = null; + } + showGeneralFormError(message: string): void { + this.errorMessage = message; + this.successMessage = null; + } + showGeneralFormSuccess(message: string): void { + this.successMessage = message; + this.errorMessage = null; + } } \ No newline at end of file diff --git a/src/app/components/supplier-page/supplier/supplier.component.html b/src/app/components/supplier-page/supplier/supplier.component.html index 6296999..e92dc67 100644 --- a/src/app/components/supplier-page/supplier/supplier.component.html +++ b/src/app/components/supplier-page/supplier/supplier.component.html @@ -17,7 +17,7 @@
- Supplier Portal + Supplier Chain Management System

Welcome back,

{{ user.name }}

@@ -61,12 +61,6 @@

Current Requests

- - -

View Profile

-

Check your contact and company details.

-
-
diff --git a/src/app/components/supplier-page/supplier/supplier.component.ts b/src/app/components/supplier-page/supplier/supplier.component.ts index 26df43a..ab5127b 100644 --- a/src/app/components/supplier-page/supplier/supplier.component.ts +++ b/src/app/components/supplier-page/supplier/supplier.component.ts @@ -15,8 +15,8 @@ interface Tab { }) export class SupplierDashboard { user = { - name: "John Smith", - role: "Warehouse Manager", + name: "Samuel Johnson", + role: "Supplier", employeeId: "LM-2023-089", department: "Logistics", location: "North Distribution Center", diff --git a/src/app/layouts/dashboard-layout/dashboard-layout.component.html b/src/app/layouts/dashboard-layout/dashboard-layout.component.html index c9ed643..a678dd5 100644 --- a/src/app/layouts/dashboard-layout/dashboard-layout.component.html +++ b/src/app/layouts/dashboard-layout/dashboard-layout.component.html @@ -2,10 +2,40 @@
- - + + +
+
+ + + +
+
+ + + + + +
+ +
-
+
\ No newline at end of file diff --git a/src/app/layouts/dashboard-layout/dashboard-layout.component.ts b/src/app/layouts/dashboard-layout/dashboard-layout.component.ts index 00fcccf..f19d36b 100644 --- a/src/app/layouts/dashboard-layout/dashboard-layout.component.ts +++ b/src/app/layouts/dashboard-layout/dashboard-layout.component.ts @@ -1,21 +1,50 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Router, NavigationEnd } from '@angular/router'; import { RouterOutlet } from '@angular/router'; -import { SidebarComponent } from "../../components/sidebar/sidebar.component"; +import { SidebarComponent } from '../../components/sidebar/sidebar.component'; import { LucideAngularModule, FileIcon, PanelLeft } from 'lucide-angular'; +import { filter } from 'rxjs/operators'; @Component({ selector: 'app-dashboard-layout', - imports: [SidebarComponent, RouterOutlet, LucideAngularModule], + standalone: true, + imports: [ + CommonModule, + RouterModule, + SidebarComponent, + RouterOutlet, + LucideAngularModule, + ], templateUrl: './dashboard-layout.component.html', - styleUrl: './dashboard-layout.component.css' + styleUrls: ['./dashboard-layout.component.css'], }) -export class DashboardLayoutComponent { +export class DashboardLayoutComponent implements OnInit { readonly FileIcon = FileIcon; - readonly panelLeft = PanelLeft - sidebarClosed : boolean = false; + readonly panelLeft = PanelLeft; + sidebarClosed = false; + isProfilePage = false; + + constructor(private router: Router) {} + + ngOnInit() { + // Check initial route + this.checkIfProfilePage(this.router.url); + + // Subscribe to route changes + this.router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe((event: NavigationEnd) => { + this.checkIfProfilePage(event.url); + }); + } + + checkIfProfilePage(url: string) { + // Check if the current URL contains 'profile' + this.isProfilePage = url.includes('/profile'); + } handleSidebarClosed() { - console.log("clicked") - this.sidebarClosed = !this.sidebarClosed + this.sidebarClosed = !this.sidebarClosed; } -} +} \ No newline at end of file