1885 lines
84 KiB
TypeScript
1885 lines
84 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useSearchParams } from 'react-router-dom';
|
||
import {
|
||
Plus,
|
||
Search,
|
||
|
||
Package,
|
||
AlertTriangle,
|
||
Database,
|
||
Truck,
|
||
Activity,
|
||
Archive,
|
||
BarChart3,
|
||
Clock,
|
||
X,
|
||
Edit2,
|
||
PlusCircle,
|
||
Eye,
|
||
ChevronLeft,
|
||
ChevronRight,
|
||
ChevronsLeft,
|
||
ChevronsRight
|
||
} from 'lucide-react';
|
||
import { motion } from 'framer-motion';
|
||
import { Card } from '../../components/Common';
|
||
import { fleetApi } from '../../api/fleet';
|
||
|
||
interface InventoryItem {
|
||
id: string;
|
||
name: string;
|
||
category: string;
|
||
totalStock: number;
|
||
minStock: number;
|
||
maxStock: number;
|
||
unit: string;
|
||
expiringSoon: number;
|
||
supplierDetails: string;
|
||
leadTimeDays: number;
|
||
vehicles: { vehicleId: string; stock: number }[];
|
||
}
|
||
|
||
|
||
|
||
interface InventoryMetadata {
|
||
categories: string[];
|
||
units: string[];
|
||
commonNames: Record<string, string[]>;
|
||
}
|
||
|
||
export const FleetInventory: React.FC = () => {
|
||
const [inventory, setInventory] = useState<InventoryItem[]>([]);
|
||
const [selectedItem, setSelectedItem] = useState<InventoryItem | null>(null);
|
||
const [loading, setLoading] = useState<boolean>(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||
const [isSearchFocused, setIsSearchFocused] = useState<boolean>(false);
|
||
const [page, setPage] = useState<number>(1);
|
||
const [itemsPerPage, setItemsPerPage] = useState<number>(5);
|
||
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
||
const [loadingRequests, setLoadingRequests] = useState<boolean>(true);
|
||
const [, setSearchParams] = useSearchParams();
|
||
|
||
// Metadata states
|
||
const [metadata, setMetadata] = useState<InventoryMetadata>({
|
||
categories: [],
|
||
units: [],
|
||
commonNames: {}
|
||
});
|
||
|
||
// Modal / Form states
|
||
const [showAddModal, setShowAddModal] = useState<boolean>(false);
|
||
const [showDetailsModal, setShowDetailsModal] = useState<boolean>(false);
|
||
const [formCategory, setFormCategory] = useState<string>('MEDICATION');
|
||
const [formNameType, setFormNameType] = useState<'standard' | 'custom'>('standard');
|
||
const [formNameStandard, setFormNameStandard] = useState<string>('');
|
||
const [formNameCustom, setFormNameCustom] = useState<string>('');
|
||
const [formUnit, setFormUnit] = useState<string>('Tablet');
|
||
const [formMinStock, setFormMinStock] = useState<number>(30);
|
||
const [formMaxStock, setFormMaxStock] = useState<number>(150);
|
||
const [formSupplier, setFormSupplier] = useState<string>('');
|
||
const [formLeadTime, setFormLeadTime] = useState<number>(3);
|
||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||
|
||
// Single Restock states
|
||
const [showSingleRestockModal, setShowSingleRestockModal] = useState<boolean>(false);
|
||
const [singleRestockQuantity, setSingleRestockQuantity] = useState<number>(100);
|
||
const [singleRestockReason, setSingleRestockReason] = useState<string>('Shipment from HealthCare Distributors');
|
||
const [singleRestockIsSubmitting, setSingleRestockIsSubmitting] = useState<boolean>(false);
|
||
const [singleRestockError, setSingleRestockError] = useState<string | null>(null);
|
||
|
||
// Bulk Restock states
|
||
interface BulkRestockRow {
|
||
itemId: string;
|
||
quantity: number;
|
||
reason: string;
|
||
}
|
||
const [showBulkRestockModal, setShowBulkRestockModal] = useState<boolean>(false);
|
||
const [bulkRestockRows, setBulkRestockRows] = useState<BulkRestockRow[]>([
|
||
{ itemId: '', quantity: 100, reason: 'Shipment from HealthCare Distributors' }
|
||
]);
|
||
const [bulkRestockIsSubmitting, setBulkRestockIsSubmitting] = useState<boolean>(false);
|
||
const [bulkRestockError, setBulkRestockError] = useState<string | null>(null);
|
||
|
||
// Assign to Vehicle states
|
||
interface AssignVehicleRow {
|
||
itemId: string;
|
||
quantity: number;
|
||
batch_number: string;
|
||
expiry_date: string;
|
||
}
|
||
const [showAssignVehicleModal, setShowAssignVehicleModal] = useState<boolean>(false);
|
||
const [assignVehicleId, setAssignVehicleId] = useState<string>('');
|
||
const [assignSupplierName, setAssignSupplierName] = useState<string>('Central Warehouse');
|
||
const [assignReason, setAssignReason] = useState<string>('Ambulance Initial Stocking');
|
||
const [assignVehicleRows, setAssignVehicleRows] = useState<AssignVehicleRow[]>([
|
||
{ itemId: '', quantity: 100, batch_number: '', expiry_date: '' }
|
||
]);
|
||
const [assignVehicleIsSubmitting, setAssignVehicleIsSubmitting] = useState<boolean>(false);
|
||
const [assignVehicleError, setAssignVehicleError] = useState<string | null>(null);
|
||
const [vehiclesList, setVehiclesList] = useState<any[]>([]);
|
||
|
||
useEffect(() => {
|
||
const fetchVehicles = async () => {
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
const userStr = localStorage.getItem('teleems_user');
|
||
let orgId = '';
|
||
if (userStr) {
|
||
const u = JSON.parse(userStr);
|
||
orgId = u.organisationId || '';
|
||
}
|
||
const res = await fleetApi.getVehicles(token, orgId);
|
||
const data = res?.data?.data || res?.data || [];
|
||
setVehiclesList(Array.isArray(data) ? data : []);
|
||
} catch (e) {
|
||
console.error('Failed to fetch vehicles:', e);
|
||
}
|
||
};
|
||
fetchVehicles();
|
||
}, []);
|
||
|
||
const handleSubmitAssignVehicle = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setAssignVehicleError(null);
|
||
setAssignVehicleIsSubmitting(true);
|
||
|
||
if (!assignVehicleId) {
|
||
setAssignVehicleError('Please select a vehicle.');
|
||
setAssignVehicleIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
if (assignVehicleRows.length === 0) {
|
||
setAssignVehicleError('Please add at least one item to assign.');
|
||
setAssignVehicleIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
const invalidRow = assignVehicleRows.find(row => !row.itemId || row.quantity <= 0);
|
||
if (invalidRow) {
|
||
setAssignVehicleError('Please ensure all rows have an item selected and a positive quantity.');
|
||
setAssignVehicleIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
|
||
const payload = {
|
||
supplier_name: assignSupplierName,
|
||
reason: assignReason,
|
||
items: assignVehicleRows.map(row => ({
|
||
itemId: row.itemId,
|
||
quantity: Number(row.quantity),
|
||
batch_number: row.batch_number || 'N/A',
|
||
expiry_date: row.expiry_date || new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0]
|
||
}))
|
||
};
|
||
|
||
await fleetApi.assignToVehicle(assignVehicleId, payload, token);
|
||
|
||
alert(`Successfully assigned inventory to vehicle!`);
|
||
setShowAssignVehicleModal(false);
|
||
} catch (err: any) {
|
||
console.error('Failed to assign to vehicle:', err);
|
||
setAssignVehicleError(err?.message || 'Failed to submit assignment. Check item details or try again.');
|
||
} finally {
|
||
setAssignVehicleIsSubmitting(false);
|
||
}
|
||
};
|
||
const handleSubmitSingleRestock = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!selectedItem) return;
|
||
setSingleRestockError(null);
|
||
setSingleRestockIsSubmitting(true);
|
||
|
||
if (singleRestockQuantity <= 0) {
|
||
setSingleRestockError('Please specify a positive quantity to restock.');
|
||
setSingleRestockIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
|
||
const payload = [
|
||
{
|
||
itemId: selectedItem.id,
|
||
quantity: Number(singleRestockQuantity),
|
||
reason: singleRestockReason || 'Routine stock replenishment'
|
||
}
|
||
];
|
||
|
||
await fleetApi.restockInventory(payload, token);
|
||
|
||
// Update stock level locally
|
||
setInventory(prev => prev.map(item => {
|
||
if (item.id === selectedItem.id) {
|
||
return {
|
||
...item,
|
||
totalStock: item.totalStock + Number(singleRestockQuantity)
|
||
};
|
||
}
|
||
return item;
|
||
}));
|
||
|
||
setSelectedItem(prev => {
|
||
if (prev && prev.id === selectedItem.id) {
|
||
return {
|
||
...prev,
|
||
totalStock: prev.totalStock + Number(singleRestockQuantity)
|
||
};
|
||
}
|
||
return prev;
|
||
});
|
||
|
||
// Show success notification and close modal
|
||
alert(`Successfully restocked ${singleRestockQuantity} units of ${selectedItem.name}!`);
|
||
setShowSingleRestockModal(false);
|
||
} catch (err: any) {
|
||
console.error('Failed to restock item:', err);
|
||
setSingleRestockError(err?.message || 'Failed to submit restock request. Please verify the connection.');
|
||
} finally {
|
||
setSingleRestockIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const handleSubmitBulkRestock = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setBulkRestockError(null);
|
||
setBulkRestockIsSubmitting(true);
|
||
|
||
// Validate rows
|
||
if (bulkRestockRows.length === 0) {
|
||
setBulkRestockError('Please add at least one item to restock.');
|
||
setBulkRestockIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
const invalidRow = bulkRestockRows.find(row => !row.itemId || row.quantity <= 0);
|
||
if (invalidRow) {
|
||
setBulkRestockError('Please ensure all rows have an item selected and a positive quantity.');
|
||
setBulkRestockIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
|
||
const payload = bulkRestockRows.map(row => ({
|
||
itemId: row.itemId,
|
||
quantity: Number(row.quantity),
|
||
reason: row.reason || 'Bulk Shipment'
|
||
}));
|
||
|
||
await fleetApi.restockInventory(payload, token);
|
||
|
||
// Update local stock levels for all successfully restocked items
|
||
setInventory(prev => prev.map(item => {
|
||
const restockMatch = bulkRestockRows.find(row => row.itemId === item.id);
|
||
if (restockMatch) {
|
||
return {
|
||
...item,
|
||
totalStock: item.totalStock + Number(restockMatch.quantity)
|
||
};
|
||
}
|
||
return item;
|
||
}));
|
||
|
||
// Update selected item details locally too if it was restocked
|
||
setSelectedItem(prev => {
|
||
if (prev) {
|
||
const restockMatch = bulkRestockRows.find(row => row.itemId === prev.id);
|
||
if (restockMatch) {
|
||
return {
|
||
...prev,
|
||
totalStock: prev.totalStock + Number(restockMatch.quantity)
|
||
};
|
||
}
|
||
}
|
||
return prev;
|
||
});
|
||
|
||
alert(`Successfully processed bulk restock for ${bulkRestockRows.length} items!`);
|
||
setShowBulkRestockModal(false);
|
||
} catch (err: any) {
|
||
console.error('Failed bulk restock:', err);
|
||
setBulkRestockError(err?.message || 'Failed to submit bulk restock. Check item compatibility or try again.');
|
||
} finally {
|
||
setBulkRestockIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
|
||
// Dynamic selector values based on category
|
||
const getSortedUnits = () => {
|
||
return metadata.units;
|
||
};
|
||
|
||
// Fetch category-specific metadata dynamically from backend when Category selection changes
|
||
useEffect(() => {
|
||
const fetchCategorySpecificMetadata = async () => {
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
if (!token) return;
|
||
|
||
const response = await fleetApi.getInventoryMetadata(token, formCategory);
|
||
const metaData = response?.data?.data || response?.data;
|
||
if (metaData) {
|
||
let categoryNames: string[] = [];
|
||
if (metaData.common_names) {
|
||
if (Array.isArray(metaData.common_names)) {
|
||
categoryNames = metaData.common_names;
|
||
} else if (metaData.common_names[formCategory]) {
|
||
categoryNames = metaData.common_names[formCategory];
|
||
} else if (typeof metaData.common_names === 'object') {
|
||
const values = Object.values(metaData.common_names);
|
||
const foundArray = values.find(val => Array.isArray(val));
|
||
if (foundArray) {
|
||
categoryNames = foundArray as string[];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (categoryNames.length === 0) {
|
||
categoryNames = [];
|
||
}
|
||
|
||
setMetadata(prev => ({
|
||
categories: prev.categories,
|
||
units: metaData.units || prev.units,
|
||
commonNames: {
|
||
...prev.commonNames,
|
||
[formCategory]: categoryNames
|
||
}
|
||
}));
|
||
}
|
||
} catch (err) {
|
||
console.warn(`Failed to fetch metadata for category ${formCategory}:`, err);
|
||
}
|
||
};
|
||
|
||
fetchCategorySpecificMetadata();
|
||
}, [formCategory]);
|
||
|
||
// Synchronize dynamic selector value standard selection lists and category preferred unit formats
|
||
useEffect(() => {
|
||
// 1. Manage Dynamic Medicine Selection list
|
||
const list = metadata.commonNames[formCategory] || [];
|
||
if (list.length > 0) {
|
||
setFormNameStandard(list[0]);
|
||
setFormNameType('standard');
|
||
} else {
|
||
setFormNameStandard('');
|
||
setFormNameType('custom');
|
||
}
|
||
|
||
// 2. Manage Dynamic Category-Based Unit pre-selection
|
||
if (metadata.units.length > 0) {
|
||
setFormUnit(metadata.units[0]);
|
||
}
|
||
}, [formCategory, metadata.commonNames, metadata.units]);
|
||
|
||
// Edit Modal / Form states
|
||
const [showEditModal, setShowEditModal] = useState<boolean>(false);
|
||
const [editCategoryId, setEditCategoryId] = useState<string>('');
|
||
const [formEditCategory, setFormEditCategory] = useState<string>('MEDICATION');
|
||
const [formEditName, setFormEditName] = useState<string>('');
|
||
const [formEditUnit, setFormEditUnit] = useState<string>('Tablet');
|
||
const [formEditMinStock, setFormEditMinStock] = useState<number>(30);
|
||
const [formEditMaxStock, setFormEditMaxStock] = useState<number>(150);
|
||
const [formEditSupplier, setFormEditSupplier] = useState<string>('');
|
||
const [formEditLeadTime, setFormEditLeadTime] = useState<number>(3);
|
||
const [isEditingSubmitting, setIsEditingSubmitting] = useState<boolean>(false);
|
||
const [editSubmitError, setEditSubmitError] = useState<string | null>(null);
|
||
|
||
const getSortedEditUnits = () => {
|
||
return metadata.units;
|
||
};
|
||
|
||
const handleOpenEditModal = (item: InventoryItem) => {
|
||
setEditCategoryId(item.id);
|
||
setFormEditCategory(item.category);
|
||
setFormEditName(item.name);
|
||
const formattedUnit = item.unit.charAt(0).toUpperCase() + item.unit.slice(1).toLowerCase();
|
||
setFormEditUnit(formattedUnit);
|
||
setFormEditMinStock(item.minStock);
|
||
setFormEditMaxStock(item.maxStock);
|
||
setFormEditSupplier(item.supplierDetails);
|
||
setFormEditLeadTime(item.leadTimeDays);
|
||
setEditSubmitError(null);
|
||
setShowEditModal(true);
|
||
};
|
||
|
||
// Fetch category-specific metadata dynamically from backend when edit Category selection changes
|
||
useEffect(() => {
|
||
if (showEditModal) {
|
||
const fetchCategorySpecificMetadata = async () => {
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
if (!token) return;
|
||
|
||
const response = await fleetApi.getInventoryMetadata(token, formEditCategory);
|
||
const metaData = response?.data?.data || response?.data;
|
||
if (metaData) {
|
||
let categoryNames: string[] = [];
|
||
if (metaData.common_names) {
|
||
if (Array.isArray(metaData.common_names)) {
|
||
categoryNames = metaData.common_names;
|
||
} else if (metaData.common_names[formEditCategory]) {
|
||
categoryNames = metaData.common_names[formEditCategory];
|
||
} else if (typeof metaData.common_names === 'object') {
|
||
const values = Object.values(metaData.common_names);
|
||
const foundArray = values.find(val => Array.isArray(val));
|
||
if (foundArray) {
|
||
categoryNames = foundArray as string[];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (categoryNames.length === 0) {
|
||
categoryNames = [];
|
||
}
|
||
|
||
setMetadata(prev => ({
|
||
categories: prev.categories,
|
||
units: metaData.units || prev.units,
|
||
commonNames: {
|
||
...prev.commonNames,
|
||
[formEditCategory]: categoryNames
|
||
}
|
||
}));
|
||
}
|
||
} catch (err) {
|
||
console.warn(`Failed to fetch metadata for category ${formEditCategory}:`, err);
|
||
}
|
||
};
|
||
|
||
fetchCategorySpecificMetadata();
|
||
}
|
||
}, [formEditCategory, showEditModal]);
|
||
|
||
const handleSubmitEditStock = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setEditSubmitError(null);
|
||
setIsEditingSubmitting(true);
|
||
|
||
if (!formEditName.trim()) {
|
||
setEditSubmitError('Supply item name is required.');
|
||
setIsEditingSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
|
||
const payload = {
|
||
name: formEditName,
|
||
category: formEditCategory,
|
||
unit: formEditUnit,
|
||
min_stock_threshold: Number(formEditMinStock),
|
||
max_stock_level: Number(formEditMaxStock),
|
||
supplier_details: formEditSupplier || 'Generic Supplier Distributors',
|
||
lead_time_days: Number(formEditLeadTime)
|
||
};
|
||
|
||
await fleetApi.updateInventoryMaster(editCategoryId, payload, token);
|
||
|
||
setInventory(prev => prev.map(item => {
|
||
if (item.id === editCategoryId) {
|
||
return {
|
||
...item,
|
||
name: formEditName,
|
||
category: formEditCategory,
|
||
unit: formEditUnit.toUpperCase(),
|
||
minStock: Number(formEditMinStock),
|
||
maxStock: Number(formEditMaxStock),
|
||
supplierDetails: formEditSupplier || 'Generic Supplier Distributors',
|
||
leadTimeDays: Number(formEditLeadTime)
|
||
};
|
||
}
|
||
return item;
|
||
}));
|
||
|
||
setSelectedItem(prev => {
|
||
if (prev && prev.id === editCategoryId) {
|
||
return {
|
||
...prev,
|
||
name: formEditName,
|
||
category: formEditCategory,
|
||
unit: formEditUnit.toUpperCase(),
|
||
minStock: Number(formEditMinStock),
|
||
maxStock: Number(formEditMaxStock),
|
||
supplierDetails: formEditSupplier || 'Generic Supplier Distributors',
|
||
leadTimeDays: Number(formEditLeadTime)
|
||
};
|
||
}
|
||
return prev;
|
||
});
|
||
|
||
setShowEditModal(false);
|
||
} catch (err: any) {
|
||
console.error('Failed to update inventory master:', err);
|
||
setEditSubmitError(err?.message || 'Failed to update supply item details.');
|
||
} finally {
|
||
setIsEditingSubmitting(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
const fetchInventoryAndMetadata = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
if (!token) {
|
||
setError('Authentication token not found. Please log in again.');
|
||
return;
|
||
}
|
||
|
||
// Fetch master list and metadata in parallel
|
||
const [response, metaResponse] = await Promise.all([
|
||
fleetApi.getInventoryMaster(token),
|
||
fleetApi.getInventoryMetadata(token).catch(e => {
|
||
console.warn('Metadata fetch failed, using fallbacks:', e);
|
||
return null;
|
||
})
|
||
]);
|
||
|
||
// Process metadata response
|
||
if (metaResponse) {
|
||
const metaData = metaResponse?.data?.data || metaResponse?.data;
|
||
if (metaData) {
|
||
setMetadata({
|
||
categories: metaData.categories || [],
|
||
units: metaData.units || [],
|
||
commonNames: metaData.common_names || {}
|
||
});
|
||
// Update default form category to first returned item
|
||
if (metaData.categories && metaData.categories.length > 0) {
|
||
setFormCategory(metaData.categories[0]);
|
||
}
|
||
if (metaData.units && metaData.units.length > 0) {
|
||
setFormUnit(metaData.units[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle the various structures returned by the backend
|
||
let rawList = response?.data?.data || response?.data || (Array.isArray(response) ? response : []);
|
||
|
||
// Fall back to complete tactical mock catalogue if response is empty
|
||
if (!rawList || rawList.length === 0) {
|
||
rawList = [];
|
||
}
|
||
|
||
// Map the API fields to our UI-friendly model with rich, simulated telemetry where needed
|
||
const mappedList: InventoryItem[] = rawList.map((item: any, index: number) => {
|
||
const minStock = item.min_stock_threshold || 20;
|
||
const maxStock = item.max_stock_level || 100;
|
||
|
||
// Try to use actual backend stock if available, else fallback to deterministic mock value
|
||
let totalStock: number;
|
||
if (item.totalStock !== undefined) {
|
||
totalStock = Number(item.totalStock);
|
||
} else if (item.current_stock !== undefined) {
|
||
totalStock = Number(item.current_stock);
|
||
} else if (item.total_stock !== undefined) {
|
||
totalStock = Number(item.total_stock);
|
||
} else if (item.stock !== undefined) {
|
||
totalStock = Number(item.stock);
|
||
} else if (item.quantity !== undefined) {
|
||
totalStock = Number(item.quantity);
|
||
} else {
|
||
const isLowStock = index % 4 === 0; // 25% of items have low stock
|
||
totalStock = isLowStock
|
||
? Math.floor(minStock * 0.6)
|
||
: Math.floor(minStock + (maxStock - minStock) * 0.4);
|
||
}
|
||
|
||
// Expiring soon count
|
||
const expiringSoon = index % 5 === 0 ? Math.floor(minStock * 0.15) : 0;
|
||
|
||
// Distribute stock across mock vehicles
|
||
const v1 = Math.floor(totalStock * 0.4);
|
||
const v2 = Math.floor(totalStock * 0.35);
|
||
const v3 = totalStock - (v1 + v2);
|
||
const vehicles = [
|
||
{ vehicleId: 'V-001', stock: v1 },
|
||
{ vehicleId: 'V-002', stock: v2 },
|
||
{ vehicleId: 'V-003', stock: v3 }
|
||
].filter(v => v.stock > 0);
|
||
|
||
return {
|
||
id: item.id || `ITM-00${index + 1}`,
|
||
name: item.name || 'Unknown Supply Item',
|
||
category: item.category || 'GENERAL',
|
||
totalStock,
|
||
minStock,
|
||
maxStock,
|
||
unit: item.unit_of_measure || 'UNITS',
|
||
expiringSoon,
|
||
supplierDetails: item.supplier_details || 'Generic Supplier Distributors',
|
||
leadTimeDays: item.lead_time_days || 3,
|
||
vehicles
|
||
};
|
||
});
|
||
|
||
setInventory(mappedList);
|
||
|
||
// Auto-select the first item on load
|
||
if (mappedList.length > 0) {
|
||
setSelectedItem(mappedList[0]);
|
||
}
|
||
} catch (err: any) {
|
||
console.error('Failed to fetch inventory master:', err);
|
||
setError(err?.message || 'Failed to sync with tactical stock catalog.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchInventoryAndMetadata();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const fetchRequests = async () => {
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
if (!token) return;
|
||
const res = await fleetApi.getPendingRestockRequests(token);
|
||
const data = res?.data?.data || res?.data || [];
|
||
setPendingRequests(Array.isArray(data) ? data : []);
|
||
} catch (err) {
|
||
console.error('Failed to fetch pending requests:', err);
|
||
} finally {
|
||
setLoadingRequests(false);
|
||
}
|
||
};
|
||
fetchRequests();
|
||
}, []);
|
||
|
||
const handleSubmitAddStock = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setSubmitError(null);
|
||
setIsSubmitting(true);
|
||
|
||
const nameToSubmit = formNameType === 'standard'
|
||
? formNameStandard
|
||
: formNameCustom;
|
||
|
||
if (!nameToSubmit.trim()) {
|
||
setSubmitError('Supply item name is required.');
|
||
setIsSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('teleems_token') || '';
|
||
|
||
const payload = [
|
||
{
|
||
name: nameToSubmit,
|
||
category: formCategory,
|
||
unit: formUnit,
|
||
min_stock_threshold: Number(formMinStock),
|
||
max_stock_level: Number(formMaxStock),
|
||
supplier_details: formSupplier || 'Generic Supplier Distributors',
|
||
lead_time_days: Number(formLeadTime)
|
||
}
|
||
];
|
||
|
||
await fleetApi.createInventoryMaster(payload, token);
|
||
|
||
// Build UI-friendly representation for local state update
|
||
const newLocalItem: InventoryItem = {
|
||
id: `ITM-${Math.floor(Math.random() * 900000 + 100000)}`,
|
||
name: nameToSubmit,
|
||
category: formCategory,
|
||
totalStock: Math.floor(formMinStock + (formMaxStock - formMinStock) * 0.4), // healthy initial stock
|
||
minStock: Number(formMinStock),
|
||
maxStock: Number(formMaxStock),
|
||
unit: formUnit.toUpperCase(), // consistent capitalization
|
||
expiringSoon: 0,
|
||
supplierDetails: formSupplier || 'Generic Supplier Distributors',
|
||
leadTimeDays: Number(formLeadTime),
|
||
vehicles: []
|
||
};
|
||
|
||
setInventory(prev => [newLocalItem, ...prev]);
|
||
setSelectedItem(newLocalItem);
|
||
|
||
// Reset modal state
|
||
setShowAddModal(false);
|
||
setFormNameCustom('');
|
||
setFormSupplier('');
|
||
} catch (err: any) {
|
||
console.error('Failed to create inventory master:', err);
|
||
setSubmitError(err?.message || 'Failed to register the new supply item.');
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Filter inventory based on search input
|
||
const filteredInventory = inventory.filter(item =>
|
||
item.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
item.category?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
item.id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||
);
|
||
|
||
const totalPages = Math.max(1, Math.ceil(filteredInventory.length / itemsPerPage));
|
||
const safePage = Math.min(page, totalPages);
|
||
const pageData = filteredInventory.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage);
|
||
|
||
// Dynamic calculations for stats
|
||
const totalSKUs = inventory.length;
|
||
const lowStockCount = inventory.filter(item => item.totalStock < item.minStock).length;
|
||
const expiringSoonCount = inventory.reduce((sum, item) => sum + (item.expiringSoon || 0), 0);
|
||
const consumptionMtd = `₹${(inventory.length * 4.2 || 42.5).toFixed(1)}k`;
|
||
|
||
return (
|
||
<div className="fleet-inventory animate-in fade-in duration-500">
|
||
{/* Top Stats */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px', marginBottom: '24px' }}>
|
||
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.05)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||
<div style={{ color: 'var(--accent-cyan)' }}><Archive size={20} /></div>
|
||
<span style={{ fontSize: '0.65rem', fontWeight: 900, color: 'var(--accent-green)' }}>+12%</span>
|
||
</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 900 }}>{loading ? '...' : totalSKUs}</div>
|
||
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>TOTAL SKUs</div>
|
||
</div>
|
||
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(239, 68, 68, 0.2)', background: 'rgba(239, 68, 68, 0.02)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||
<div style={{ color: '#EF4444' }}><AlertTriangle size={20} /></div>
|
||
</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#EF4444' }}>{loading ? '...' : lowStockCount}</div>
|
||
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>LOW STOCK ITEMS</div>
|
||
</div>
|
||
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(245, 158, 11, 0.2)', background: 'rgba(245, 158, 11, 0.02)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||
<div style={{ color: '#F59E0B' }}><Clock size={20} /></div>
|
||
</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#F59E0B' }}>{loading ? '...' : expiringSoonCount}</div>
|
||
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>EXPIRING (30D)</div>
|
||
</div>
|
||
<div className="glass" style={{ padding: '20px', borderRadius: '16px', border: '1px solid rgba(168, 85, 247, 0.2)', background: 'rgba(168, 85, 247, 0.02)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||
<div style={{ color: '#A855F7' }}><Package size={20} /></div>
|
||
</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#A855F7' }}>{loadingRequests ? '...' : pendingRequests.length}</div>
|
||
<div style={{ fontSize: '0.7rem', opacity: 0.5, textTransform: 'uppercase' }}>PENDING REQUESTS</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '10px',
|
||
background: '#FFFFFF',
|
||
padding: '10px 16px',
|
||
borderRadius: '12px',
|
||
border: isSearchFocused ? '1px solid #06B6D4' : '1px solid #CBD5E1',
|
||
boxShadow: isSearchFocused ? '0 0 0 3px rgba(6, 182, 212, 0.15)' : '0 2px 4px rgba(15, 23, 42, 0.01)',
|
||
transition: 'all 0.2s ease',
|
||
flex: 1,
|
||
position: 'relative'
|
||
}}>
|
||
<Search size={16} color={isSearchFocused ? "#06B6D4" : "#64748B"} style={{ transition: 'color 0.2s' }} />
|
||
<input
|
||
type="text"
|
||
placeholder="Filter item master by name, category, or batch..."
|
||
value={searchQuery}
|
||
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
|
||
onFocus={() => setIsSearchFocused(true)}
|
||
onBlur={() => setIsSearchFocused(false)}
|
||
className="stations-search-input"
|
||
style={{
|
||
background: 'transparent',
|
||
border: 'none',
|
||
color: '#0F172A',
|
||
fontSize: '0.875rem',
|
||
width: '100%',
|
||
outline: 'none',
|
||
paddingRight: searchQuery ? '24px' : '0'
|
||
}}
|
||
/>
|
||
{searchQuery && (
|
||
<button
|
||
onClick={() => { setSearchQuery(''); setPage(1); }}
|
||
style={{
|
||
position: 'absolute',
|
||
right: '12px',
|
||
background: 'transparent',
|
||
border: 'none',
|
||
cursor: 'pointer',
|
||
color: '#94A3B8',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: '2px',
|
||
borderRadius: '50%',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.color = '#475569'}
|
||
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setSubmitError(null);
|
||
setShowAddModal(true);
|
||
}}
|
||
className="btn-primary"
|
||
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||
>
|
||
<Plus size={16} /> ADD MASTER
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => {
|
||
setSearchParams({ tab: 'warehouse' });
|
||
}}
|
||
className="btn-secondary"
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px',
|
||
background: 'rgba(16, 185, 129, 0.1)',
|
||
border: '1px solid rgba(16, 185, 129, 0.3)',
|
||
color: '#10B981',
|
||
borderRadius: '12px',
|
||
padding: '8px 16px',
|
||
fontSize: '0.8125rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
transition: 'all 0.2s'
|
||
}}
|
||
onMouseEnter={e => {
|
||
e.currentTarget.style.background = 'rgba(16, 185, 129, 0.2)';
|
||
}}
|
||
onMouseLeave={e => {
|
||
e.currentTarget.style.background = 'rgba(16, 185, 129, 0.1)';
|
||
}}
|
||
>
|
||
<Database size={16} /> WAREHOUSE STOCK
|
||
</button>
|
||
</div>
|
||
|
||
<Card title="Tactical Inventory Ledger">
|
||
{loading ? (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '100px', gap: '16px', color: 'var(--text-secondary)' }}>
|
||
<Activity size={32} className="spin" style={{ color: 'var(--accent-cyan)' }} />
|
||
<span style={{ fontSize: '0.875rem', fontWeight: 600 }}>DECRYPTING STOCK CATALOG...</span>
|
||
</div>
|
||
) : error ? (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '80px 24px', textAlign: 'center', color: '#EF4444' }}>
|
||
<AlertTriangle size={48} style={{ marginBottom: '16px', opacity: 0.8 }} />
|
||
<h3 style={{ fontSize: '1.1rem', fontWeight: 700, marginBottom: '8px' }}>Tactical Catalog Offline</h3>
|
||
<p style={{ fontSize: '0.82rem', color: '#94A3B8', maxWidth: '360px', margin: '0 auto' }}>{error}</p>
|
||
</div>
|
||
) : filteredInventory.length === 0 ? (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '100px 24px', color: '#64748B' }}>
|
||
<Package size={48} style={{ opacity: 0.2, marginBottom: '16px' }} />
|
||
<h3 style={{ fontSize: '1rem', fontWeight: 700, color: '#94A3B8', marginBottom: '4px' }}>No Supply Items Match</h3>
|
||
<p style={{ fontSize: '0.8125rem' }}>Adjust search criteria or add new items to the registry.</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ textAlign: 'left', opacity: 0.5, fontSize: '0.65rem', textTransform: 'uppercase', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||
<th style={{ padding: '12px' }}>Item Name</th>
|
||
<th style={{ padding: '12px' }}>Category</th>
|
||
<th style={{ padding: '12px' }}>Min Threshold</th>
|
||
<th style={{ padding: '12px' }}>Max Level</th>
|
||
<th style={{ padding: '12px' }}>Supplier Details</th>
|
||
<th style={{ padding: '12px' }}>Lead Time</th>
|
||
<th style={{ padding: '12px', textAlign: 'right' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{pageData.map((item, idx) => (
|
||
<tr
|
||
key={item.id}
|
||
onClick={() => {
|
||
setSelectedItem(item);
|
||
setShowDetailsModal(true);
|
||
}}
|
||
style={{
|
||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||
cursor: 'pointer',
|
||
background: selectedItem?.id === item.id ? 'rgba(59, 130, 246, 0.05)' : 'transparent'
|
||
}}
|
||
className="hover-glow"
|
||
>
|
||
<td style={{ padding: '16px 12px' }}>
|
||
<div style={{ fontWeight: 700, fontSize: '0.875rem' }}>{item.name}</div>
|
||
</td>
|
||
<td style={{ padding: '16px 12px' }}>
|
||
<span style={{ fontSize: '0.7rem', fontWeight: 700, opacity: 0.8, color: 'var(--accent-cyan)', background: 'rgba(6, 182, 212, 0.08)', padding: '4px 8px', borderRadius: '6px' }}>{item.category}</span>
|
||
</td>
|
||
<td style={{ padding: '16px 12px' }}>
|
||
<div style={{ fontWeight: 800 }}>{item.minStock} <span style={{ fontSize: '0.65rem', fontWeight: 500, opacity: 0.5 }}>{item.unit}</span></div>
|
||
</td>
|
||
<td style={{ padding: '16px 12px' }}>
|
||
<div style={{ fontWeight: 800 }}>{item.maxStock} <span style={{ fontSize: '0.65rem', fontWeight: 500, opacity: 0.5 }}>{item.unit}</span></div>
|
||
</td>
|
||
<td style={{ padding: '16px 12px' }}>
|
||
<div style={{ fontSize: '0.8rem', color: '#94A3B8' }}>{item.supplierDetails}</div>
|
||
</td>
|
||
<td style={{ padding: '16px 12px' }}>
|
||
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--accent-cyan)' }}>{item.leadTimeDays} days</div>
|
||
</td>
|
||
<td style={{ padding: '16px 12px', textAlign: 'right' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px' }}>
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setSelectedItem(item);
|
||
setShowDetailsModal(true);
|
||
}}
|
||
style={{
|
||
background: 'rgba(2, 132, 199, 0.1)',
|
||
border: 'none',
|
||
color: '#0284C7',
|
||
borderRadius: '6px',
|
||
padding: '6px',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
transition: 'all 0.2s'
|
||
}}
|
||
onMouseEnter={e => {
|
||
e.currentTarget.style.background = 'rgba(2, 132, 199, 0.2)';
|
||
}}
|
||
onMouseLeave={e => {
|
||
e.currentTarget.style.background = 'rgba(2, 132, 199, 0.1)';
|
||
}}
|
||
title="View Details"
|
||
>
|
||
<Eye size={12} />
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleOpenEditModal(item);
|
||
}}
|
||
style={{
|
||
background: 'rgba(245, 158, 11, 0.1)',
|
||
border: 'none',
|
||
color: '#D97706',
|
||
borderRadius: '6px',
|
||
padding: '6px',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
transition: 'all 0.2s'
|
||
}}
|
||
onMouseEnter={e => {
|
||
e.currentTarget.style.background = 'rgba(245, 158, 11, 0.2)';
|
||
}}
|
||
onMouseLeave={e => {
|
||
e.currentTarget.style.background = 'rgba(245, 158, 11, 0.1)';
|
||
}}
|
||
title="Edit Item"
|
||
>
|
||
<Edit2 size={12} />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* Pagination Controls */}
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 20px', borderTop: '1px solid rgba(15, 23, 42, 0.08)' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||
<span style={{ fontSize: '0.78rem', color: '#475569' }}>
|
||
Showing <strong style={{ color: '#0F172A' }}>{filteredInventory.length === 0 ? 0 : (safePage - 1) * itemsPerPage + 1}–{Math.min(safePage * itemsPerPage, filteredInventory.length)}</strong> of <strong style={{ color: '#0F172A' }}>{filteredInventory.length}</strong> items
|
||
</span>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<span style={{ fontSize: '0.75rem', color: '#64748B' }}>Rows per page:</span>
|
||
<select
|
||
value={itemsPerPage}
|
||
onChange={e => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
|
||
style={{ padding: '4px 8px', borderRadius: 6, border: '1px solid #CBD5E1', background: '#FFFFFF', color: '#0F172A', fontSize: '0.75rem', outline: 'none', cursor: 'pointer' }}
|
||
>
|
||
<option value={5}>5</option>
|
||
<option value={10}>10</option>
|
||
<option value={20}>20</option>
|
||
<option value={50}>50</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||
<button onClick={() => setPage(1)} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === 1 ? '#94A3B8' : '#475569', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}><ChevronsLeft size={15} /></button>
|
||
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={safePage === 1} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === 1 ? '#94A3B8' : '#475569', cursor: safePage === 1 ? 'not-allowed' : 'pointer' }}><ChevronLeft size={15} /></button>
|
||
|
||
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
||
.filter(p => p === 1 || p === totalPages || Math.abs(p - safePage) <= 1)
|
||
.map((p, i, arr) => (
|
||
<React.Fragment key={p}>
|
||
{i > 0 && arr[i - 1] !== p - 1 && <span style={{ padding: '0 4px', color: '#94A3B8' }}>...</span>}
|
||
<button onClick={() => setPage(p)} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: 'none', background: p === safePage ? 'linear-gradient(135deg,#06B6D4,#3B82F6)' : '#F1F5F9', color: p === safePage ? '#fff' : '#475569', fontWeight: p === safePage ? 700 : 500, cursor: 'pointer', fontSize: '0.82rem' }}>{p}</button>
|
||
</React.Fragment>
|
||
))}
|
||
|
||
<button onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === totalPages ? '#94A3B8' : '#475569', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronRight size={15} /></button>
|
||
<button onClick={() => setPage(totalPages)} disabled={safePage === totalPages} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid #E2E8F0', background: '#F1F5F9', color: safePage === totalPages ? '#94A3B8' : '#475569', cursor: safePage === totalPages ? 'not-allowed' : 'pointer' }}><ChevronsRight size={15} /></button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</Card>
|
||
|
||
|
||
</div>
|
||
|
||
{/* Supply Details Modal */}
|
||
{showDetailsModal && selectedItem && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
background: 'rgba(15, 23, 42, 0.4)',
|
||
backdropFilter: 'blur(8px)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1400,
|
||
padding: '24px 16px',
|
||
boxSizing: 'border-box',
|
||
animation: 'fadeIn 0.25s ease-out'
|
||
}}>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||
style={{
|
||
width: '100%',
|
||
maxWidth: '560px',
|
||
maxHeight: '100%',
|
||
background: '#FFFFFF',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '20px',
|
||
padding: '30px',
|
||
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.15)',
|
||
fontFamily: "'Inter', sans-serif",
|
||
boxSizing: 'border-box',
|
||
overflowY: 'auto'
|
||
}}
|
||
>
|
||
{/* Header */}
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', borderBottom: '1px solid #F1F5F9', paddingBottom: '16px' }}>
|
||
<div>
|
||
<h3 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#0F172A', margin: 0, letterSpacing: '-0.5px' }}>{selectedItem.name}</h3>
|
||
<span style={{ fontSize: '0.7rem', fontWeight: 700, color: '#0284C7', textTransform: 'uppercase', letterSpacing: '1px' }}>{selectedItem.category}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowDetailsModal(false)}
|
||
style={{ background: 'transparent', border: 'none', color: '#64748B', cursor: 'pointer', fontSize: '1.1rem' }}
|
||
onMouseEnter={e => e.currentTarget.style.color = '#0F172A'}
|
||
onMouseLeave={e => e.currentTarget.style.color = '#64748B'}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* Metrics */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '20px' }}>
|
||
<div style={{ padding: '16px', borderRadius: '12px', background: '#F8FAFC', border: '1px solid #E2E8F0' }}>
|
||
<div style={{ fontSize: '0.65rem', fontWeight: 700, color: '#64748B', marginBottom: '4px', textTransform: 'uppercase' }}>STOCK GAP</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: selectedItem.totalStock < selectedItem.minStock ? '#EF4444' : '#22C55E' }}>
|
||
{selectedItem.totalStock - selectedItem.minStock} <span style={{ fontSize: '0.8rem', fontWeight: 500, color: '#64748B' }}>{selectedItem.unit}</span>
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: '16px', borderRadius: '12px', background: '#F8FAFC', border: '1px solid #E2E8F0' }}>
|
||
<div style={{ fontSize: '0.65rem', fontWeight: 700, color: '#64748B', marginBottom: '4px', textTransform: 'uppercase' }}>REORDER POINT</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 900, color: '#0F172A' }}>
|
||
{selectedItem.minStock} <span style={{ fontSize: '0.8rem', fontWeight: 500, color: '#64748B' }}>{selectedItem.unit}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Supplier Card */}
|
||
<div style={{ padding: '16px', borderRadius: '12px', background: 'rgba(2, 132, 199, 0.02)', border: '1px solid rgba(2, 132, 199, 0.1)', marginBottom: '20px' }}>
|
||
<h4 style={{ fontSize: '0.75rem', fontWeight: 800, textTransform: 'uppercase', color: '#0284C7', marginBottom: '12px', margin: 0 }}>Supplier & Logistics</h4>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '12px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.8rem' }}>
|
||
<span style={{ color: '#64748B' }}>Distributor</span>
|
||
<span style={{ fontWeight: 700, color: '#0F172A' }}>{selectedItem.supplierDetails}</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.8rem' }}>
|
||
<span style={{ color: '#64748B' }}>Procurement Lead Time</span>
|
||
<span style={{ fontWeight: 700, color: '#0284C7' }}>{selectedItem.leadTimeDays} days</span>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.8rem' }}>
|
||
<span style={{ color: '#64748B' }}>Target Max Level</span>
|
||
<span style={{ fontWeight: 700, color: '#0F172A' }}>{selectedItem.maxStock} {selectedItem.unit}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Vehicles Card */}
|
||
<div style={{ marginBottom: '0px' }}>
|
||
<h4 style={{ fontSize: '0.75rem', fontWeight: 800, textTransform: 'uppercase', color: '#64748B', marginBottom: '12px', margin: 0 }}>Ambulance Stock Distribution</h4>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '12px', maxHeight: '180px', overflowY: 'auto' }}>
|
||
{selectedItem.vehicles.length === 0 ? (
|
||
<div style={{ padding: '16px', borderRadius: '12px', background: '#F8FAFC', border: '1px dashed #E2E8F0', textAlign: 'center', fontSize: '0.8rem', color: '#64748B' }}>
|
||
No active vehicle allocations found.
|
||
</div>
|
||
) : (
|
||
selectedItem.vehicles.map((v, i) => (
|
||
<div key={i} style={{ padding: '10px 14px', borderRadius: '10px', background: '#F8FAFC', border: '1px solid #E2E8F0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||
<Truck size={14} style={{ color: '#64748B' }} />
|
||
<span style={{ fontSize: '0.8rem', fontWeight: 700, color: '#0F172A' }}>{v.vehicleId}</span>
|
||
</div>
|
||
<div style={{ fontWeight: 800, color: v.stock < 5 ? '#EF4444' : '#0F172A', fontSize: '0.8rem' }}>
|
||
{v.stock} {selectedItem.unit}
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
|
||
<style>{`@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .spin { animation: spin 1s linear infinite; }`}</style>
|
||
|
||
{/* Register Supply Item Modal */}
|
||
{showAddModal && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
background: 'rgba(15, 23, 42, 0.4)',
|
||
backdropFilter: 'blur(8px)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1500,
|
||
animation: 'fadeIn 0.25s ease-out'
|
||
}}>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||
style={{
|
||
width: '100%',
|
||
maxWidth: '640px',
|
||
background: '#FFFFFF',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '20px',
|
||
padding: '32px',
|
||
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.15)',
|
||
fontFamily: "'Inter', sans-serif",
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', borderBottom: '1px solid #F1F5F9', paddingBottom: '16px' }}>
|
||
<div>
|
||
<h3 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#0F172A', margin: 0, letterSpacing: '-0.5px' }}>Register Supply Item</h3>
|
||
<span style={{ fontSize: '0.75rem', color: '#0284C7', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '1px' }}>Inventory Master registry</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowAddModal(false)}
|
||
style={{ background: 'transparent', border: 'none', color: '#64748B', cursor: 'pointer', fontSize: '1.1rem' }}
|
||
onMouseEnter={e => e.currentTarget.style.color = '#0F172A'}
|
||
onMouseLeave={e => e.currentTarget.style.color = '#64748B'}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmitAddStock} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||
{submitError && (
|
||
<div style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', color: '#EF4444', padding: '12px 16px', borderRadius: '10px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<AlertTriangle size={16} />
|
||
<span>{submitError}</span>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||
{/* Column 1 */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Category</label>
|
||
<select
|
||
value={formCategory}
|
||
onChange={(e) => setFormCategory(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
cursor: 'pointer',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
{metadata.categories.map(cat => (
|
||
<option key={cat} value={cat} style={{ background: '#FFFFFF', color: '#0F172A' }}>{cat}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Supply Name</label>
|
||
{metadata.commonNames[formCategory]?.length > 0 && (
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormNameType('standard')}
|
||
style={{ background: formNameType === 'standard' ? 'rgba(2,132,199,0.15)' : 'transparent', border: 'none', color: formNameType === 'standard' ? '#0284C7' : '#64748B', fontSize: '0.65rem', fontWeight: 700, padding: '2px 6px', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
STANDARD
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormNameType('custom')}
|
||
style={{ background: formNameType === 'custom' ? 'rgba(2,132,199,0.15)' : 'transparent', border: 'none', color: formNameType === 'custom' ? '#0284C7' : '#64748B', fontSize: '0.65rem', fontWeight: 700, padding: '2px 6px', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
CUSTOM
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{formNameType === 'standard' && metadata.commonNames[formCategory]?.length > 0 ? (
|
||
<select
|
||
value={formNameStandard}
|
||
onChange={(e) => setFormNameStandard(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
cursor: 'pointer',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
{metadata.commonNames[formCategory].map(name => (
|
||
<option key={name} value={name} style={{ background: '#FFFFFF', color: '#0F172A' }}>{name}</option>
|
||
))}
|
||
</select>
|
||
) : (
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. Paracetamol 500mg"
|
||
value={formNameCustom}
|
||
onChange={(e) => setFormNameCustom(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Unit of Measure</label>
|
||
<select
|
||
value={formUnit}
|
||
onChange={(e) => setFormUnit(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
cursor: 'pointer',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
{getSortedUnits().map(unit => (
|
||
<option key={unit} value={unit} style={{ background: '#FFFFFF', color: '#0F172A' }}>{unit}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Column 2 */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Min Threshold</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={formMinStock}
|
||
onChange={(e) => setFormMinStock(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Max Stock</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={formMaxStock}
|
||
onChange={(e) => setFormMaxStock(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Supplier Details</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. HealthCare Distributors"
|
||
value={formSupplier}
|
||
onChange={(e) => setFormSupplier(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Lead Time (Days)</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
max="30"
|
||
value={formLeadTime}
|
||
onChange={(e) => setFormLeadTime(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', borderTop: '1px solid #F1F5F9', paddingTop: '20px', marginTop: '10px' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddModal(false)}
|
||
className="btn-ghost"
|
||
style={{ padding: '10px 20px', fontSize: '0.8rem', color: '#475569' }}
|
||
>
|
||
CANCEL
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="btn-primary"
|
||
disabled={isSubmitting}
|
||
style={{ padding: '10px 24px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px' }}
|
||
>
|
||
{isSubmitting ? (
|
||
<>
|
||
<Activity size={14} className="spin" /> REGISTERING...
|
||
</>
|
||
) : (
|
||
'REGISTER ITEM'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Supply Item Modal */}
|
||
{showEditModal && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
background: 'rgba(15, 23, 42, 0.4)',
|
||
backdropFilter: 'blur(8px)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1500,
|
||
animation: 'fadeIn 0.25s ease-out'
|
||
}}>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||
style={{
|
||
width: '100%',
|
||
maxWidth: '680px',
|
||
background: '#FFFFFF',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '20px',
|
||
padding: '30px',
|
||
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.15)',
|
||
position: 'relative',
|
||
fontFamily: "'Inter', sans-serif",
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px', borderBottom: '1px solid #F1F5F9', paddingBottom: '16px' }}>
|
||
<div>
|
||
<h2 style={{ fontSize: '1.25rem', fontWeight: 800, margin: 0, color: '#0F172A', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Edit Supply Item</h2>
|
||
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: '#0284C7', textTransform: 'uppercase', letterSpacing: '1px', marginTop: '4px' }}>UPDATE STOCK CATALOG</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowEditModal(false)}
|
||
style={{ background: 'none', border: 'none', color: '#64748B', cursor: 'pointer', transition: 'color 0.2s' }}
|
||
onMouseEnter={(e) => e.currentTarget.style.color = '#0F172A'}
|
||
onMouseLeave={(e) => e.currentTarget.style.color = '#64748B'}
|
||
>
|
||
<X size={20} />
|
||
</button>
|
||
</div>
|
||
|
||
{editSubmitError && (
|
||
<div style={{ padding: '12px 16px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '10px', color: '#EF4444', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '20px' }}>
|
||
<AlertTriangle size={14} /> {editSubmitError}
|
||
</div>
|
||
)}
|
||
|
||
<form onSubmit={handleSubmitEditStock}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||
{/* Column 1 */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Category</label>
|
||
<select
|
||
value={formEditCategory}
|
||
onChange={(e) => setFormEditCategory(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
cursor: 'pointer',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
{metadata.categories.map(cat => (
|
||
<option key={cat} value={cat} style={{ background: '#FFFFFF', color: '#0F172A' }}>{cat}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Supply Name</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. Paracetamol 500mg"
|
||
value={formEditName}
|
||
onChange={(e) => setFormEditName(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Unit of Measure</label>
|
||
<select
|
||
value={formEditUnit}
|
||
onChange={(e) => setFormEditUnit(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
cursor: 'pointer',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
{getSortedEditUnits().map(unit => (
|
||
<option key={unit} value={unit} style={{ background: '#FFFFFF', color: '#0F172A' }}>{unit}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Column 2 */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Min Threshold</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={formEditMinStock}
|
||
onChange={(e) => setFormEditMinStock(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Max Stock</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={formEditMaxStock}
|
||
onChange={(e) => setFormEditMaxStock(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Supplier Details</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. HealthCare Distributors"
|
||
value={formEditSupplier}
|
||
onChange={(e) => setFormEditSupplier(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Lead Time (Days)</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
max="30"
|
||
value={formEditLeadTime}
|
||
onChange={(e) => setFormEditLeadTime(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', borderTop: '1px solid #F1F5F9', paddingTop: '20px', marginTop: '10px' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowEditModal(false)}
|
||
className="btn-ghost"
|
||
style={{ padding: '10px 20px', fontSize: '0.8rem', color: '#475569' }}
|
||
>
|
||
CANCEL
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="btn-primary"
|
||
disabled={isEditingSubmitting}
|
||
style={{ padding: '10px 24px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px' }}
|
||
>
|
||
{isEditingSubmitting ? (
|
||
<>
|
||
<Activity size={14} className="spin" /> UPDATING...
|
||
</>
|
||
) : (
|
||
'UPDATE ITEM'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Single Restock Modal */}
|
||
{showSingleRestockModal && selectedItem && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
background: 'rgba(15, 23, 42, 0.4)',
|
||
backdropFilter: 'blur(8px)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1500,
|
||
animation: 'fadeIn 0.25s ease-out'
|
||
}}>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||
style={{
|
||
width: '100%',
|
||
maxWidth: '520px',
|
||
background: '#FFFFFF',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '20px',
|
||
padding: '30px',
|
||
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.15)',
|
||
fontFamily: "'Inter', sans-serif",
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', borderBottom: '1px solid #F1F5F9', paddingBottom: '16px' }}>
|
||
<div>
|
||
<h3 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#0F172A', margin: 0, letterSpacing: '-0.5px' }}>Restock Supply Item</h3>
|
||
<span style={{ fontSize: '0.75rem', color: '#0284C7', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '1px' }}>Single intake replenishing</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowSingleRestockModal(false)}
|
||
style={{ background: 'transparent', border: 'none', color: '#64748B', cursor: 'pointer', fontSize: '1.1rem' }}
|
||
onMouseEnter={e => e.currentTarget.style.color = '#0F172A'}
|
||
onMouseLeave={e => e.currentTarget.style.color = '#64748B'}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmitSingleRestock} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||
{singleRestockError && (
|
||
<div style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', color: '#EF4444', padding: '12px 16px', borderRadius: '10px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<AlertTriangle size={16} />
|
||
<span>{singleRestockError}</span>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Item Name</label>
|
||
<input
|
||
type="text"
|
||
value={selectedItem.name}
|
||
disabled
|
||
style={{
|
||
background: '#F1F5F9',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#475569',
|
||
fontSize: '0.85rem',
|
||
width: '100%',
|
||
boxSizing: 'border-box',
|
||
cursor: 'not-allowed',
|
||
fontWeight: 600
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Current Stock</label>
|
||
<div style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
fontWeight: 700
|
||
}}>
|
||
{selectedItem.totalStock} {selectedItem.unit}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Restock Quantity ({selectedItem.unit})</label>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={singleRestockQuantity}
|
||
onChange={(e) => setSingleRestockQuantity(Number(e.target.value))}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
width: '100%',
|
||
boxSizing: 'border-box',
|
||
fontWeight: 700
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||
<label style={{ fontSize: '0.7rem', fontWeight: 700, color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Reason for Restock</label>
|
||
<select
|
||
value={singleRestockReason}
|
||
onChange={(e) => setSingleRestockReason(e.target.value)}
|
||
style={{
|
||
background: '#F8FAFC',
|
||
border: '1px solid #E2E8F0',
|
||
borderRadius: '10px',
|
||
padding: '10px 14px',
|
||
color: '#0F172A',
|
||
fontSize: '0.85rem',
|
||
outline: 'none',
|
||
cursor: 'pointer',
|
||
width: '100%',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
>
|
||
<option value="Shipment from HealthCare Distributors">Shipment from HealthCare Distributors</option>
|
||
<option value="Shipment from MediStore">Shipment from MediStore</option>
|
||
<option value="Routine Stock Replenishment">Routine Stock Replenishment</option>
|
||
<option value="Emergency Intake">Emergency Intake</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', borderTop: '1px solid #F1F5F9', paddingTop: '20px', marginTop: '10px' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowSingleRestockModal(false)}
|
||
className="btn-ghost"
|
||
style={{ padding: '10px 20px', fontSize: '0.8rem', color: '#475569', background: 'transparent', border: 'none', cursor: 'pointer', fontWeight: 600 }}
|
||
>
|
||
CANCEL
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="btn-primary"
|
||
disabled={singleRestockIsSubmitting}
|
||
style={{ padding: '10px 24px', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '8px', background: '#0284C7', color: '#FFFFFF', border: 'none', borderRadius: '8px', fontWeight: 700, cursor: 'pointer' }}
|
||
>
|
||
{singleRestockIsSubmitting ? (
|
||
<>
|
||
<Activity size={14} className="spin" /> RESTOCKING...
|
||
</>
|
||
) : (
|
||
'SUBMIT RESTOCK'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
);
|
||
};
|