+
+
+
+
+
+ Order Management
+
+
+ Current Requests & History
+
+
+ Manage incoming orders and view your delivery history
+
+
+
+
+
+
+
+
+
+
+
+
Pending Orders
+
{{ pendingCount }}
+
+
+
+ Accepted Orders
+
+
{{ acceptedCount }}
+
+
+
+ Delivered Orders
+
+
{{ receivedCount }}
+
+
+
+
+
+
+
Loading requests...
+
+
+
+
+
+ Error!
+ {{ errorMessage }}
+
+
+
+
0">
+
+
+ Active Orders
+
+
+
+
+
+
+
+ | Request ID |
+ Request Date |
+ Product |
+ Quantity |
+ Delivery Deadline |
+ Status |
+ Actions |
+
+
+
+
+ |
+ {{ request.id }}
+ |
+
+ {{ request.requestDate | date : "shortDate" }}
+ |
+ {{ request.productName }} |
+ {{ request.quantity | number }} |
+
+ {{ request.deadline | date : "shortDate" }}
+ |
+
+
+ {{ request.status }}
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+ No active orders found.
+ |
+
+
+
+
+
+
+
+
0">
+
+
+ Order History
+
+
+
+
+
+
+
+ | Request ID |
+ Request Date |
+ Product |
+ Quantity |
+ Outcome Date |
+
+ Status |
+ Actions |
+
+
+
+
+
+
+ |
+ {{ 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...
+
+
+
+
+
+
+ Error!
+ {{ errorMessage }}
+
+
+
@@ -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 Name |
- SKU |
- Unit Price |
- Actions |
-
-
-
-
- | {{ product.name }} |
- {{ product.sku }} |
- {{ product.price | currency }} |
-
- Edit
- Delete
- |
-
-
- | You haven't added any products yet. |
-
-
-
+
Your Product Catalogue
+
0">
+
+
+
+
+ | Product Name |
+ SKU |
+ General Price |
+ Priced Warehouses |
+ Actions |
+
+
+
+
+ | {{ product.name }} |
+ {{ product.sku }} |
+ {{ product.price | currency:'USD':'symbol':'1.2-2' }} |
+
+ 0; else noWarehouses">
+
+ {{ wh }}
+
+
+ Not priced in any warehouse
+ |
+
+
+ Set/Update Price
+
+ |
+
+
+
+
+ No products found in your catalogue.
+ |
+
+
+
+
-
-
-
- © 2025 Supply Chain Management System. All rights reserved.
+
+
+
+
+
+
+
+
Add Product to Warehouse
+ ×
+
+
+
+
+
+
+
+
+
+
Set/Update Price for Warehouse
+ ×
+
+
+
+
+
+
+ © 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 @@
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