Delete legacy alerts
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
InventoryItem,
|
||||
StockLevel,
|
||||
StockMovement,
|
||||
StockAlert,
|
||||
InventorySearchParams,
|
||||
CreateInventoryItemRequest,
|
||||
UpdateInventoryItemRequest,
|
||||
@@ -31,7 +30,6 @@ interface UseInventoryReturn {
|
||||
items: InventoryItem[];
|
||||
stockLevels: Record<string, StockLevel>;
|
||||
movements: StockMovement[];
|
||||
alerts: StockAlert[];
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
@@ -54,9 +52,6 @@ interface UseInventoryReturn {
|
||||
adjustStock: (itemId: string, adjustment: StockAdjustmentRequest) => Promise<StockMovement | null>;
|
||||
loadMovements: (params?: any) => Promise<void>;
|
||||
|
||||
// Alerts
|
||||
loadAlerts: () => Promise<void>;
|
||||
acknowledgeAlert: (alertId: string) => Promise<boolean>;
|
||||
|
||||
// Dashboard
|
||||
loadDashboard: () => Promise<void>;
|
||||
@@ -69,7 +64,6 @@ interface UseInventoryReturn {
|
||||
|
||||
interface UseInventoryDashboardReturn {
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
alerts: StockAlert[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
@@ -95,7 +89,6 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
const [items, setItems] = useState<InventoryItem[]>([]);
|
||||
const [stockLevels, setStockLevels] = useState<Record<string, StockLevel>>({});
|
||||
const [movements, setMovements] = useState<StockMovement[]>([]);
|
||||
const [alerts, setAlerts] = useState<StockAlert[]>([]);
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -278,34 +271,6 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load alerts
|
||||
const loadAlerts = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
const alertsData = await inventoryService.getStockAlerts(tenantId);
|
||||
setAlerts(alertsData);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading alerts:', err);
|
||||
// Don't show toast error for this as it's not critical for forecast page
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Acknowledge alert
|
||||
const acknowledgeAlert = useCallback(async (alertId: string): Promise<boolean> => {
|
||||
if (!tenantId) return false;
|
||||
|
||||
try {
|
||||
await inventoryService.acknowledgeAlert(tenantId, alertId);
|
||||
setAlerts(prev => prev.map(a =>
|
||||
a.id === alertId ? { ...a, is_acknowledged: true, acknowledged_at: new Date().toISOString() } : a
|
||||
));
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
toast.error('Error acknowledging alert');
|
||||
return false;
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Load dashboard
|
||||
const loadDashboard = useCallback(async () => {
|
||||
@@ -337,10 +302,9 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
await Promise.all([
|
||||
loadItems(),
|
||||
loadStockLevels(),
|
||||
loadAlerts(),
|
||||
loadDashboard()
|
||||
]);
|
||||
}, [loadItems, loadStockLevels, loadAlerts, loadDashboard]);
|
||||
}, [loadItems, loadStockLevels, loadDashboard]);
|
||||
|
||||
// Auto-load on mount
|
||||
useEffect(() => {
|
||||
@@ -354,7 +318,6 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
items,
|
||||
stockLevels,
|
||||
movements,
|
||||
alerts,
|
||||
dashboardData,
|
||||
isLoading,
|
||||
error,
|
||||
@@ -372,10 +335,6 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
adjustStock,
|
||||
loadMovements,
|
||||
|
||||
// Alerts
|
||||
loadAlerts,
|
||||
acknowledgeAlert,
|
||||
|
||||
// Dashboard
|
||||
loadDashboard,
|
||||
|
||||
@@ -391,7 +350,6 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
|
||||
const { tenantId } = useTenantId();
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [alerts, setAlerts] = useState<StockAlert[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -402,13 +360,9 @@ export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [dashboard, alertsData] = await Promise.all([
|
||||
inventoryService.getDashboardData(tenantId),
|
||||
inventoryService.getStockAlerts(tenantId)
|
||||
]);
|
||||
const dashboard = await inventoryService.getDashboardData(tenantId);
|
||||
|
||||
setDashboardData(dashboard);
|
||||
setAlerts(alertsData);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Error loading dashboard';
|
||||
setError(errorMessage);
|
||||
@@ -425,7 +379,6 @@ export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
|
||||
|
||||
return {
|
||||
dashboardData,
|
||||
alerts,
|
||||
isLoading,
|
||||
error,
|
||||
refresh
|
||||
|
||||
@@ -105,22 +105,6 @@ export interface StockMovement {
|
||||
batch_info?: StockBatch;
|
||||
}
|
||||
|
||||
export interface StockAlert {
|
||||
id: string;
|
||||
item_id: string;
|
||||
alert_type: 'low_stock' | 'expired' | 'expiring_soon' | 'overstock';
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
threshold_value?: number;
|
||||
current_value?: number;
|
||||
is_acknowledged: boolean;
|
||||
created_at: string;
|
||||
acknowledged_at?: string;
|
||||
acknowledged_by?: string;
|
||||
|
||||
// Related data
|
||||
item?: InventoryItem;
|
||||
}
|
||||
|
||||
// ========== REQUEST/RESPONSE TYPES ==========
|
||||
|
||||
@@ -404,32 +388,6 @@ export class InventoryService {
|
||||
return apiClient.get(url);
|
||||
}
|
||||
|
||||
// ========== ALERTS ==========
|
||||
|
||||
/**
|
||||
* Get current stock alerts
|
||||
*/
|
||||
async getStockAlerts(tenantId: string): Promise<StockAlert[]> {
|
||||
// TODO: Map to correct endpoint when available
|
||||
return [];
|
||||
// return apiClient.get(`/tenants/${tenantId}/inventory/alerts`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge alert
|
||||
*/
|
||||
async acknowledgeAlert(tenantId: string, alertId: string): Promise<void> {
|
||||
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/alerts/${alertId}/acknowledge`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk acknowledge alerts
|
||||
*/
|
||||
async bulkAcknowledgeAlerts(tenantId: string, alertIds: string[]): Promise<void> {
|
||||
return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/alerts/bulk-acknowledge`, {
|
||||
alert_ids: alertIds
|
||||
});
|
||||
}
|
||||
|
||||
// ========== DASHBOARD & ANALYTICS ==========
|
||||
|
||||
@@ -771,23 +729,6 @@ export class InventoryService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventory alerts
|
||||
*/
|
||||
async getInventoryAlerts(tenantId: string, params?: {
|
||||
alert_type?: string;
|
||||
severity?: string;
|
||||
status?: string;
|
||||
item_id?: string;
|
||||
limit?: number;
|
||||
}): Promise<any[]> {
|
||||
try {
|
||||
return await apiClient.get(`/tenants/${tenantId}/inventory/alerts`, { params });
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching inventory alerts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restock recommendations
|
||||
|
||||
@@ -344,20 +344,6 @@ export class OrdersService {
|
||||
return apiClient.get(`${this.basePath}/analytics/seasonal`, { params });
|
||||
}
|
||||
|
||||
// Alerts
|
||||
async getOrderAlerts(params?: {
|
||||
severity?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}): Promise<any[]> {
|
||||
return apiClient.get(`${this.basePath}/alerts`, { params });
|
||||
}
|
||||
|
||||
async acknowledgeAlert(alertId: string): Promise<void> {
|
||||
return apiClient.post(`${this.basePath}/alerts/${alertId}/acknowledge`);
|
||||
}
|
||||
|
||||
async resolveAlert(alertId: string, resolution?: string): Promise<void> {
|
||||
return apiClient.post(`${this.basePath}/alerts/${alertId}/resolve`, { resolution });
|
||||
}
|
||||
}
|
||||
@@ -66,13 +66,11 @@ export interface ProductionDashboardData {
|
||||
equipment_in_use: number;
|
||||
current_efficiency: number;
|
||||
todays_production: number;
|
||||
alerts_count: number;
|
||||
};
|
||||
efficiency_trend: { date: string; efficiency: number }[];
|
||||
quality_trend: { date: string; quality: number }[];
|
||||
equipment_status: Equipment[];
|
||||
active_batches: ProductionBatch[];
|
||||
alerts: any[];
|
||||
}
|
||||
|
||||
export interface BatchCreateRequest {
|
||||
@@ -174,8 +172,7 @@ export class ProductionService {
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
estimated_completion: string;
|
||||
alerts: any[];
|
||||
}> {
|
||||
}> {
|
||||
return apiClient.get(`${this.basePath}/batches/${batchId}/status`);
|
||||
}
|
||||
|
||||
@@ -295,20 +292,5 @@ export class ProductionService {
|
||||
return apiClient.get(`${this.basePath}/analytics/quality`, { params });
|
||||
}
|
||||
|
||||
// Alerts
|
||||
async getProductionAlerts(params?: {
|
||||
severity?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}): Promise<any[]> {
|
||||
return apiClient.get(`${this.basePath}/alerts`, { params });
|
||||
}
|
||||
|
||||
async acknowledgeAlert(alertId: string): Promise<void> {
|
||||
return apiClient.post(`${this.basePath}/alerts/${alertId}/acknowledge`);
|
||||
}
|
||||
|
||||
async resolveAlert(alertId: string, resolution?: string): Promise<void> {
|
||||
return apiClient.post(`${this.basePath}/alerts/${alertId}/resolve`, { resolution });
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,6 @@ export interface SupplierDashboardData {
|
||||
quality_scores: number[];
|
||||
cost_savings: number[];
|
||||
};
|
||||
alerts: any[];
|
||||
contract_expirations: { supplier_name: string; days_until_expiry: number }[];
|
||||
}
|
||||
|
||||
@@ -320,23 +319,7 @@ export class SuppliersService {
|
||||
return apiClient.get(`${this.basePath}/analytics/risk-analysis`);
|
||||
}
|
||||
|
||||
// Alerts
|
||||
async getSupplierAlerts(params?: {
|
||||
severity?: string;
|
||||
status?: string;
|
||||
supplier_id?: string;
|
||||
limit?: number;
|
||||
}): Promise<any[]> {
|
||||
return apiClient.get(`${this.basePath}/alerts`, { params });
|
||||
}
|
||||
|
||||
async acknowledgeAlert(alertId: string): Promise<void> {
|
||||
return apiClient.post(`${this.basePath}/alerts/${alertId}/acknowledge`);
|
||||
}
|
||||
|
||||
async resolveAlert(alertId: string, resolution?: string): Promise<void> {
|
||||
return apiClient.post(`${this.basePath}/alerts/${alertId}/resolve`, { resolution });
|
||||
}
|
||||
|
||||
// Additional methods for hooks compatibility
|
||||
async getSupplierStatistics(): Promise<SupplierStatistics> {
|
||||
|
||||
@@ -21,12 +21,12 @@ const InventoryDashboardWidget: React.FC<InventoryDashboardWidgetProps> = ({
|
||||
onViewInventory,
|
||||
className = ''
|
||||
}) => {
|
||||
const { dashboardData, alerts, isLoading, error, refresh } = useInventoryDashboard();
|
||||
const { dashboardData, isLoading, error, refresh } = useInventoryDashboard();
|
||||
|
||||
// Get alert counts
|
||||
const criticalAlerts = alerts.filter(a => !a.is_acknowledged && a.severity === 'critical').length;
|
||||
const lowStockAlerts = alerts.filter(a => !a.is_acknowledged && a.alert_type === 'low_stock').length;
|
||||
const expiringAlerts = alerts.filter(a => !a.is_acknowledged && a.alert_type === 'expiring_soon').length;
|
||||
const criticalAlerts = 0;
|
||||
const lowStockAlerts = 0;
|
||||
const expiringAlerts = 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Package,
|
||||
TrendingDown,
|
||||
CheckCircle,
|
||||
X,
|
||||
Filter,
|
||||
Bell,
|
||||
BellOff,
|
||||
Calendar
|
||||
} from 'lucide-react';
|
||||
|
||||
import { StockAlert } from '../../api/services/inventory.service';
|
||||
|
||||
interface StockAlertsPanelProps {
|
||||
alerts: StockAlert[];
|
||||
onAcknowledge?: (alertId: string) => void;
|
||||
onAcknowledgeAll?: (alertIds: string[]) => void;
|
||||
onViewItem?: (itemId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type AlertFilter = 'all' | 'unacknowledged' | 'low_stock' | 'expired' | 'expiring_soon';
|
||||
|
||||
const StockAlertsPanel: React.FC<StockAlertsPanelProps> = ({
|
||||
alerts,
|
||||
onAcknowledge,
|
||||
onAcknowledgeAll,
|
||||
onViewItem,
|
||||
className = ''
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<AlertFilter>('all');
|
||||
const [selectedAlerts, setSelectedAlerts] = useState<Set<string>>(new Set());
|
||||
|
||||
// Filter alerts based on current filter
|
||||
const filteredAlerts = alerts.filter(alert => {
|
||||
switch (filter) {
|
||||
case 'unacknowledged':
|
||||
return !alert.is_acknowledged;
|
||||
case 'low_stock':
|
||||
return alert.alert_type === 'low_stock';
|
||||
case 'expired':
|
||||
return alert.alert_type === 'expired';
|
||||
case 'expiring_soon':
|
||||
return alert.alert_type === 'expiring_soon';
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Get alert icon
|
||||
const getAlertIcon = (alert: StockAlert) => {
|
||||
switch (alert.alert_type) {
|
||||
case 'low_stock':
|
||||
return <TrendingDown className="w-5 h-5" />;
|
||||
case 'expired':
|
||||
return <X className="w-5 h-5" />;
|
||||
case 'expiring_soon':
|
||||
return <Clock className="w-5 h-5" />;
|
||||
case 'overstock':
|
||||
return <Package className="w-5 h-5" />;
|
||||
default:
|
||||
return <AlertTriangle className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Get alert color classes
|
||||
const getAlertClasses = (alert: StockAlert) => {
|
||||
const baseClasses = 'border-l-4';
|
||||
|
||||
if (alert.is_acknowledged) {
|
||||
return `${baseClasses} border-gray-300 bg-gray-50`;
|
||||
}
|
||||
|
||||
switch (alert.severity) {
|
||||
case 'critical':
|
||||
return `${baseClasses} border-red-500 bg-red-50`;
|
||||
case 'high':
|
||||
return `${baseClasses} border-orange-500 bg-orange-50`;
|
||||
case 'medium':
|
||||
return `${baseClasses} border-yellow-500 bg-yellow-50`;
|
||||
case 'low':
|
||||
return `${baseClasses} border-blue-500 bg-blue-50`;
|
||||
default:
|
||||
return `${baseClasses} border-gray-500 bg-gray-50`;
|
||||
}
|
||||
};
|
||||
|
||||
// Get alert text color
|
||||
const getAlertTextColor = (alert: StockAlert) => {
|
||||
if (alert.is_acknowledged) {
|
||||
return 'text-gray-600';
|
||||
}
|
||||
|
||||
switch (alert.severity) {
|
||||
case 'critical':
|
||||
return 'text-red-700';
|
||||
case 'high':
|
||||
return 'text-orange-700';
|
||||
case 'medium':
|
||||
return 'text-yellow-700';
|
||||
case 'low':
|
||||
return 'text-blue-700';
|
||||
default:
|
||||
return 'text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
// Get alert icon color
|
||||
const getAlertIconColor = (alert: StockAlert) => {
|
||||
if (alert.is_acknowledged) {
|
||||
return 'text-gray-400';
|
||||
}
|
||||
|
||||
switch (alert.severity) {
|
||||
case 'critical':
|
||||
return 'text-red-500';
|
||||
case 'high':
|
||||
return 'text-orange-500';
|
||||
case 'medium':
|
||||
return 'text-yellow-500';
|
||||
case 'low':
|
||||
return 'text-blue-500';
|
||||
default:
|
||||
return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
// Handle alert selection
|
||||
const toggleAlertSelection = (alertId: string) => {
|
||||
const newSelection = new Set(selectedAlerts);
|
||||
if (newSelection.has(alertId)) {
|
||||
newSelection.delete(alertId);
|
||||
} else {
|
||||
newSelection.add(alertId);
|
||||
}
|
||||
setSelectedAlerts(newSelection);
|
||||
};
|
||||
|
||||
// Handle acknowledge all selected
|
||||
const handleAcknowledgeSelected = () => {
|
||||
if (onAcknowledgeAll && selectedAlerts.size > 0) {
|
||||
onAcknowledgeAll(Array.from(selectedAlerts));
|
||||
setSelectedAlerts(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
// Format time ago
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
|
||||
|
||||
if (diffInHours < 1) {
|
||||
return 'Hace menos de 1 hora';
|
||||
} else if (diffInHours < 24) {
|
||||
return `Hace ${diffInHours} horas`;
|
||||
} else {
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
return `Hace ${diffInDays} días`;
|
||||
}
|
||||
};
|
||||
|
||||
// Get filter counts
|
||||
const getFilterCounts = () => {
|
||||
return {
|
||||
all: alerts.length,
|
||||
unacknowledged: alerts.filter(a => !a.is_acknowledged).length,
|
||||
low_stock: alerts.filter(a => a.alert_type === 'low_stock').length,
|
||||
expired: alerts.filter(a => a.alert_type === 'expired').length,
|
||||
expiring_soon: alerts.filter(a => a.alert_type === 'expiring_soon').length,
|
||||
};
|
||||
};
|
||||
|
||||
const filterCounts = getFilterCounts();
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-sm border ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Bell className="w-5 h-5 text-gray-600" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Alertas de Stock</h2>
|
||||
{filterCounts.unacknowledged > 0 && (
|
||||
<span className="bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full">
|
||||
{filterCounts.unacknowledged} pendientes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedAlerts.size > 0 && (
|
||||
<button
|
||||
onClick={handleAcknowledgeSelected}
|
||||
className="flex items-center space-x-1 px-3 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>Confirmar ({selectedAlerts.size})</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ key: 'all', label: 'Todas', count: filterCounts.all },
|
||||
{ key: 'unacknowledged', label: 'Pendientes', count: filterCounts.unacknowledged },
|
||||
{ key: 'low_stock', label: 'Stock Bajo', count: filterCounts.low_stock },
|
||||
{ key: 'expired', label: 'Vencidas', count: filterCounts.expired },
|
||||
{ key: 'expiring_soon', label: 'Por Vencer', count: filterCounts.expiring_soon },
|
||||
].map(({ key, label, count }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setFilter(key as AlertFilter)}
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||
filter === key
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{label} ({count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alerts List */}
|
||||
<div className="divide-y">
|
||||
{filteredAlerts.length === 0 ? (
|
||||
<div className="p-8 text-center">
|
||||
<BellOff className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{filter === 'all' ? 'No hay alertas' : 'No hay alertas con este filtro'}
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
{filter === 'all'
|
||||
? 'Tu inventario está en buen estado'
|
||||
: 'Prueba con un filtro diferente'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredAlerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`p-4 hover:bg-gray-50 transition-colors ${getAlertClasses(alert)}`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
{/* Selection checkbox */}
|
||||
{!alert.is_acknowledged && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAlerts.has(alert.id)}
|
||||
onChange={() => toggleAlertSelection(alert.id)}
|
||||
className="mt-1 w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Alert Icon */}
|
||||
<div className={`mt-0.5 ${getAlertIconColor(alert)}`}>
|
||||
{getAlertIcon(alert)}
|
||||
</div>
|
||||
|
||||
{/* Alert Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className={`font-medium ${getAlertTextColor(alert)}`}>
|
||||
{alert.item?.name || 'Producto desconocido'}
|
||||
</h4>
|
||||
<p className={`text-sm mt-1 ${getAlertTextColor(alert)}`}>
|
||||
{alert.message}
|
||||
</p>
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{formatTimeAgo(alert.created_at)}</span>
|
||||
</div>
|
||||
|
||||
{alert.threshold_value && alert.current_value && (
|
||||
<span>
|
||||
Umbral: {alert.threshold_value} | Actual: {alert.current_value}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="capitalize">
|
||||
Severidad: {alert.severity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Acknowledged Info */}
|
||||
{alert.is_acknowledged && alert.acknowledged_at && (
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
<span>✓ Confirmada {formatTimeAgo(alert.acknowledged_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
{onViewItem && alert.item_id && (
|
||||
<button
|
||||
onClick={() => onViewItem(alert.item_id)}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||
>
|
||||
Ver producto
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!alert.is_acknowledged && onAcknowledge && (
|
||||
<button
|
||||
onClick={() => onAcknowledge(alert.id)}
|
||||
className="text-green-600 hover:text-green-800 text-sm font-medium"
|
||||
>
|
||||
Confirmar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer with bulk actions */}
|
||||
{filteredAlerts.length > 0 && filterCounts.unacknowledged > 0 && (
|
||||
<div className="p-4 border-t bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">
|
||||
{filterCounts.unacknowledged} alertas pendientes
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onAcknowledgeAll) {
|
||||
const unacknowledgedIds = alerts
|
||||
.filter(a => !a.is_acknowledged)
|
||||
.map(a => a.id);
|
||||
onAcknowledgeAll(unacknowledgedIds);
|
||||
}
|
||||
}}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||
>
|
||||
Confirmar todas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockAlertsPanel;
|
||||
@@ -1,180 +0,0 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, Cloud, Package, Clock, ChevronRight } from 'lucide-react';
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
|
||||
severity: 'high' | 'medium' | 'low';
|
||||
title: string;
|
||||
description: string;
|
||||
action?: string;
|
||||
time?: string;
|
||||
}
|
||||
|
||||
interface CriticalAlertsProps {
|
||||
alerts?: Alert[];
|
||||
onAlertClick?: (alertId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CriticalAlerts: React.FC<CriticalAlertsProps> = ({
|
||||
alerts = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'stock',
|
||||
severity: 'high',
|
||||
title: 'Stock Bajo',
|
||||
description: 'Pan integral: solo 5 unidades',
|
||||
action: 'Hacer más',
|
||||
time: '10:30'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'weather',
|
||||
severity: 'medium',
|
||||
title: 'Lluvia Esperada',
|
||||
description: 'Precipitaciones 14:00 - 17:00',
|
||||
action: 'Ajustar producción',
|
||||
time: '14:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'order',
|
||||
severity: 'low',
|
||||
title: 'Pedido Especial',
|
||||
description: 'Tarta de cumpleaños Ana - Viernes',
|
||||
action: 'Ver detalles',
|
||||
time: 'Viernes'
|
||||
}
|
||||
],
|
||||
onAlertClick,
|
||||
className = ''
|
||||
}) => {
|
||||
const getAlertIcon = (type: Alert['type']) => {
|
||||
switch (type) {
|
||||
case 'stock': return Package;
|
||||
case 'weather': return Cloud;
|
||||
case 'order': return Clock;
|
||||
case 'production': return AlertTriangle;
|
||||
default: return AlertTriangle;
|
||||
}
|
||||
};
|
||||
|
||||
const getAlertColors = (severity: Alert['severity']) => {
|
||||
switch (severity) {
|
||||
case 'high': return {
|
||||
bg: 'bg-red-50 border-red-200',
|
||||
icon: 'text-red-600',
|
||||
title: 'text-red-900',
|
||||
description: 'text-red-700'
|
||||
};
|
||||
case 'medium': return {
|
||||
bg: 'bg-yellow-50 border-yellow-200',
|
||||
icon: 'text-yellow-600',
|
||||
title: 'text-yellow-900',
|
||||
description: 'text-yellow-700'
|
||||
};
|
||||
case 'low': return {
|
||||
bg: 'bg-blue-50 border-blue-200',
|
||||
icon: 'text-blue-600',
|
||||
title: 'text-blue-900',
|
||||
description: 'text-blue-700'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const visibleAlerts = alerts.slice(0, 3); // Show max 3 alerts
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<AlertTriangle className="h-5 w-5 mr-2 text-orange-600" />
|
||||
Atención Requerida
|
||||
</h3>
|
||||
{alerts.length > 3 && (
|
||||
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
|
||||
+{alerts.length - 3} más
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Alerts List */}
|
||||
{visibleAlerts.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{visibleAlerts.map((alert) => {
|
||||
const IconComponent = getAlertIcon(alert.type);
|
||||
const colors = getAlertColors(alert.severity);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={alert.id}
|
||||
onClick={() => onAlertClick?.(alert.id)}
|
||||
className={`${colors.bg} border rounded-lg p-3 cursor-pointer hover:shadow-sm transition-all duration-200`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<IconComponent className={`h-4 w-4 mt-0.5 ${colors.icon} flex-shrink-0`} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className={`text-sm font-medium ${colors.title}`}>
|
||||
{alert.title}
|
||||
</h4>
|
||||
<p className={`text-xs ${colors.description} mt-1`}>
|
||||
{alert.description}
|
||||
</p>
|
||||
{alert.action && (
|
||||
<p className={`text-xs ${colors.icon} font-medium mt-1`}>
|
||||
→ {alert.action}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-3">
|
||||
{alert.time && (
|
||||
<span className={`text-xs ${colors.description}`}>
|
||||
{alert.time}
|
||||
</span>
|
||||
)}
|
||||
<ChevronRight className={`h-3 w-3 ${colors.icon}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<AlertTriangle className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<h4 className="text-sm font-medium text-gray-900">Todo bajo control</h4>
|
||||
<p className="text-xs text-gray-500 mt-1">No hay alertas que requieran atención</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Summary */}
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-100">
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span className="flex items-center">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full mr-1"></div>
|
||||
{alerts.filter(a => a.severity === 'high').length} Urgentes
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-1"></div>
|
||||
{alerts.filter(a => a.severity === 'medium').length} Importantes
|
||||
</span>
|
||||
</div>
|
||||
<button className="text-xs text-gray-600 hover:text-gray-900 font-medium">
|
||||
Ver todas →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CriticalAlerts;
|
||||
@@ -1,4 +1,3 @@
|
||||
export { default as CriticalAlerts } from './CriticalAlerts';
|
||||
export { default as OrderSuggestions } from './OrderSuggestions';
|
||||
export { default as QuickActions } from './QuickActions';
|
||||
export { default as QuickOverview } from './QuickOverview';
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, TrendingDown, Package, Clock, Euro } from 'lucide-react';
|
||||
|
||||
export interface BusinessAlert {
|
||||
id: string;
|
||||
type: 'stockout_risk' | 'overstock' | 'revenue_loss' | 'quality_risk' | 'weather_impact';
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
product: string;
|
||||
message: string;
|
||||
action: string;
|
||||
impact?: {
|
||||
type: 'revenue' | 'units' | 'percentage';
|
||||
value: number;
|
||||
currency?: string;
|
||||
};
|
||||
urgency?: 'immediate' | 'today' | 'this_week';
|
||||
}
|
||||
|
||||
interface AlertCardProps {
|
||||
alert: BusinessAlert;
|
||||
onAction?: (alertId: string, actionType: string) => void;
|
||||
}
|
||||
|
||||
const getSeverityConfig = (severity: BusinessAlert['severity']) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return {
|
||||
bgColor: 'bg-red-50',
|
||||
borderColor: 'border-red-200',
|
||||
iconColor: 'text-red-600',
|
||||
textColor: 'text-red-900',
|
||||
actionColor: 'bg-red-100 hover:bg-red-200 text-red-800'
|
||||
};
|
||||
case 'high':
|
||||
return {
|
||||
bgColor: 'bg-orange-50',
|
||||
borderColor: 'border-orange-200',
|
||||
iconColor: 'text-orange-600',
|
||||
textColor: 'text-orange-900',
|
||||
actionColor: 'bg-orange-100 hover:bg-orange-200 text-orange-800'
|
||||
};
|
||||
case 'medium':
|
||||
return {
|
||||
bgColor: 'bg-yellow-50',
|
||||
borderColor: 'border-yellow-200',
|
||||
iconColor: 'text-yellow-600',
|
||||
textColor: 'text-yellow-900',
|
||||
actionColor: 'bg-yellow-100 hover:bg-yellow-200 text-yellow-800'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bgColor: 'bg-blue-50',
|
||||
borderColor: 'border-blue-200',
|
||||
iconColor: 'text-blue-600',
|
||||
textColor: 'text-blue-900',
|
||||
actionColor: 'bg-blue-100 hover:bg-blue-200 text-blue-800'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getAlertIcon = (type: BusinessAlert['type']) => {
|
||||
switch (type) {
|
||||
case 'stockout_risk':
|
||||
return Package;
|
||||
case 'overstock':
|
||||
return TrendingDown;
|
||||
case 'revenue_loss':
|
||||
return Euro;
|
||||
case 'quality_risk':
|
||||
return Clock;
|
||||
case 'weather_impact':
|
||||
return AlertTriangle;
|
||||
default:
|
||||
return AlertTriangle;
|
||||
}
|
||||
};
|
||||
|
||||
const getUrgencyLabel = (urgency?: BusinessAlert['urgency']) => {
|
||||
switch (urgency) {
|
||||
case 'immediate':
|
||||
return { label: 'URGENTE', color: 'bg-red-100 text-red-800' };
|
||||
case 'today':
|
||||
return { label: 'HOY', color: 'bg-orange-100 text-orange-800' };
|
||||
case 'this_week':
|
||||
return { label: 'ESTA SEMANA', color: 'bg-blue-100 text-blue-800' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const AlertCard: React.FC<AlertCardProps> = ({ alert, onAction }) => {
|
||||
const config = getSeverityConfig(alert.severity);
|
||||
const Icon = getAlertIcon(alert.type);
|
||||
const urgencyInfo = getUrgencyLabel(alert.urgency);
|
||||
|
||||
const handleAction = () => {
|
||||
onAction?.(alert.id, 'primary_action');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${config.bgColor} ${config.borderColor} border rounded-lg p-4 shadow-sm`}>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className={`${config.iconColor} mt-0.5`}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className={`font-medium ${config.textColor}`}>
|
||||
{alert.product}
|
||||
</h4>
|
||||
{urgencyInfo && (
|
||||
<span className={`px-2 py-1 text-xs font-bold rounded ${urgencyInfo.color}`}>
|
||||
{urgencyInfo.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className={`text-sm mt-1 ${config.textColor.replace('900', '700')}`}>
|
||||
{alert.message}
|
||||
</p>
|
||||
|
||||
{alert.impact && (
|
||||
<div className={`text-sm font-medium mt-2 ${config.textColor}`}>
|
||||
{alert.impact.type === 'revenue' && (
|
||||
<>Impacto: -{alert.impact.value}{alert.impact.currency || '€'}</>
|
||||
)}
|
||||
{alert.impact.type === 'units' && (
|
||||
<>Unidades afectadas: {alert.impact.value}</>
|
||||
)}
|
||||
{alert.impact.type === 'percentage' && (
|
||||
<>Reducción estimada: {alert.impact.value}%</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className={`px-3 py-2 text-sm font-medium rounded transition-colors ${config.actionColor}`}
|
||||
>
|
||||
{alert.action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertCard;
|
||||
@@ -1,165 +0,0 @@
|
||||
// Real API hook for Critical Alerts using backend forecast alerts
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useForecast } from '../api';
|
||||
import { useTenantId } from './useTenantId';
|
||||
|
||||
export interface RealAlert {
|
||||
id: string;
|
||||
type: 'stock' | 'weather' | 'order' | 'production' | 'system';
|
||||
severity: 'high' | 'medium' | 'low';
|
||||
title: string;
|
||||
description: string;
|
||||
action?: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export const useRealAlerts = () => {
|
||||
const [alerts, setAlerts] = useState<RealAlert[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { tenantId } = useTenantId();
|
||||
const { getForecastAlerts, acknowledgeForecastAlert } = useForecast();
|
||||
|
||||
// Transform backend forecast alerts to frontend alert format
|
||||
const transformForecastAlert = (alert: any): RealAlert => {
|
||||
// Map alert types
|
||||
let type: RealAlert['type'] = 'system';
|
||||
if (alert.alert_type?.includes('stock') || alert.alert_type?.includes('demand')) {
|
||||
type = 'stock';
|
||||
} else if (alert.alert_type?.includes('weather')) {
|
||||
type = 'weather';
|
||||
} else if (alert.alert_type?.includes('production')) {
|
||||
type = 'production';
|
||||
}
|
||||
|
||||
// Map severity
|
||||
let severity: RealAlert['severity'] = 'medium';
|
||||
if (alert.severity === 'critical' || alert.severity === 'high') {
|
||||
severity = 'high';
|
||||
} else if (alert.severity === 'low') {
|
||||
severity = 'low';
|
||||
}
|
||||
|
||||
// Generate user-friendly title and description
|
||||
let title = alert.message;
|
||||
let description = alert.message;
|
||||
|
||||
if (alert.alert_type?.includes('high_demand')) {
|
||||
title = 'Alta Demanda Prevista';
|
||||
description = `Se prevé alta demanda. ${alert.message}`;
|
||||
} else if (alert.alert_type?.includes('low_confidence')) {
|
||||
title = 'Predicción Incierta';
|
||||
description = `Baja confianza en predicción. ${alert.message}`;
|
||||
} else if (alert.alert_type?.includes('stock_risk')) {
|
||||
title = 'Riesgo de Desabastecimiento';
|
||||
description = `Posible falta de stock. ${alert.message}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: alert.id,
|
||||
type,
|
||||
severity,
|
||||
title,
|
||||
description,
|
||||
action: 'Ver detalles',
|
||||
time: new Date(alert.created_at).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
// Load real alerts from backend
|
||||
const loadAlerts = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get forecast alerts from backend
|
||||
const response = await getForecastAlerts(tenantId);
|
||||
|
||||
// Extract alerts array from paginated response
|
||||
const forecastAlerts = response.alerts || [];
|
||||
|
||||
// Filter only active alerts
|
||||
const activeAlerts = forecastAlerts.filter(alert => alert.is_active);
|
||||
|
||||
// Transform to frontend format
|
||||
const transformedAlerts = activeAlerts.map(transformForecastAlert);
|
||||
|
||||
// Sort by severity and time (most recent first)
|
||||
transformedAlerts.sort((a, b) => {
|
||||
// First by severity (high > medium > low)
|
||||
const severityOrder = { high: 3, medium: 2, low: 1 };
|
||||
const severityDiff = severityOrder[b.severity] - severityOrder[a.severity];
|
||||
if (severityDiff !== 0) return severityDiff;
|
||||
|
||||
// Then by time (most recent first)
|
||||
return b.time.localeCompare(a.time);
|
||||
});
|
||||
|
||||
setAlerts(transformedAlerts.slice(0, 3)); // Show max 3 alerts in dashboard
|
||||
} catch (error) {
|
||||
console.error('Error loading alerts:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to load alerts');
|
||||
|
||||
// Fallback to sample alerts based on common scenarios
|
||||
setAlerts(generateFallbackAlerts());
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [tenantId, getForecastAlerts]);
|
||||
|
||||
// Handle alert acknowledgment
|
||||
const handleAlertAction = useCallback(async (alertId: string) => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
await acknowledgeForecastAlert(tenantId, alertId);
|
||||
// Remove acknowledged alert from local state
|
||||
setAlerts(prev => prev.filter(alert => alert.id !== alertId));
|
||||
} catch (error) {
|
||||
console.error('Error acknowledging alert:', error);
|
||||
}
|
||||
}, [tenantId, acknowledgeForecastAlert]);
|
||||
|
||||
// Generate fallback alerts when API fails
|
||||
const generateFallbackAlerts = (): RealAlert[] => {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'fallback-1',
|
||||
type: 'stock',
|
||||
severity: 'high',
|
||||
title: 'Stock Bajo de Croissants',
|
||||
description: 'Se prevé alta demanda este fin de semana',
|
||||
action: 'Aumentar producción',
|
||||
time: timeString
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Load alerts on mount and when tenant changes
|
||||
useEffect(() => {
|
||||
loadAlerts();
|
||||
|
||||
// Refresh alerts every 5 minutes
|
||||
const interval = setInterval(loadAlerts, 5 * 60 * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [loadAlerts]);
|
||||
|
||||
return {
|
||||
alerts,
|
||||
isLoading,
|
||||
error,
|
||||
onAlertAction: handleAlertAction,
|
||||
reload: loadAlerts,
|
||||
clearError: () => setError(null),
|
||||
};
|
||||
};
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useDashboard } from '../../hooks/useDashboard';
|
||||
import { useOrderSuggestions } from '../../hooks/useOrderSuggestions';
|
||||
import { useRealAlerts } from '../../hooks/useRealAlerts';
|
||||
|
||||
// Import simplified components
|
||||
import TodayRevenue from '../../components/simple/TodayRevenue';
|
||||
import CriticalAlerts from '../../components/simple/CriticalAlerts';
|
||||
import TodayProduction from '../../components/simple/TodayProduction';
|
||||
import QuickActions from '../../components/simple/QuickActions';
|
||||
import QuickOverview from '../../components/simple/QuickOverview';
|
||||
@@ -55,11 +53,6 @@ const DashboardPage: React.FC<DashboardPageProps> = ({
|
||||
error: ordersError
|
||||
});
|
||||
|
||||
// Use real API data for alerts
|
||||
const {
|
||||
alerts: realAlerts,
|
||||
onAlertAction
|
||||
} = useRealAlerts();
|
||||
|
||||
// Transform forecast data for production component
|
||||
|
||||
@@ -163,11 +156,6 @@ const DashboardPage: React.FC<DashboardPageProps> = ({
|
||||
dailyTarget={350}
|
||||
/>
|
||||
|
||||
{/* Alerts - Real API Data */}
|
||||
<CriticalAlerts
|
||||
alerts={realAlerts}
|
||||
onAlertClick={onAlertAction}
|
||||
/>
|
||||
|
||||
{/* Quick Actions - Easy Access */}
|
||||
<QuickActions
|
||||
@@ -243,15 +231,13 @@ const DashboardPage: React.FC<DashboardPageProps> = ({
|
||||
|
||||
|
||||
{/* Success Message - When Everything is Good */}
|
||||
{realAlerts.length === 0 && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
|
||||
<div className="text-4xl mb-2">🎉</div>
|
||||
<h4 className="font-medium text-green-900">¡Todo bajo control!</h4>
|
||||
<p className="text-green-700 text-sm mt-1">
|
||||
No hay alertas activas. Tu panadería está funcionando perfectamente.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
|
||||
<div className="text-4xl mb-2">🎉</div>
|
||||
<h4 className="font-medium text-green-900">¡Todo bajo control!</h4>
|
||||
<p className="text-green-700 text-sm mt-1">
|
||||
Tu panadería está funcionando perfectamente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from '../../api/services/inventory.service';
|
||||
|
||||
import InventoryItemCard from '../../components/inventory/InventoryItemCard';
|
||||
import StockAlertsPanel from '../../components/inventory/StockAlertsPanel';
|
||||
|
||||
type ViewMode = 'grid' | 'list';
|
||||
|
||||
@@ -51,7 +50,6 @@ const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' })
|
||||
const {
|
||||
items,
|
||||
stockLevels,
|
||||
alerts,
|
||||
dashboardData,
|
||||
isLoading,
|
||||
error,
|
||||
@@ -61,14 +59,12 @@ const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' })
|
||||
updateItem,
|
||||
deleteItem,
|
||||
adjustStock,
|
||||
acknowledgeAlert,
|
||||
refresh,
|
||||
clearError
|
||||
} = useInventory();
|
||||
|
||||
// Local state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
const [showAlerts, setShowAlerts] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
search: '',
|
||||
sort_by: 'name',
|
||||
@@ -155,24 +151,12 @@ const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' })
|
||||
}
|
||||
};
|
||||
|
||||
// Handle alert acknowledgment
|
||||
const handleAcknowledgeAlert = async (alertId: string) => {
|
||||
await acknowledgeAlert(alertId);
|
||||
};
|
||||
|
||||
// Handle bulk acknowledge alerts
|
||||
const handleBulkAcknowledgeAlerts = async (alertIds: string[]) => {
|
||||
// TODO: Implement bulk acknowledge
|
||||
for (const alertId of alertIds) {
|
||||
await acknowledgeAlert(alertId);
|
||||
}
|
||||
};
|
||||
|
||||
// Get quick stats
|
||||
const getQuickStats = () => {
|
||||
const totalItems = items.length;
|
||||
const lowStockItems = alerts.filter(a => a.alert_type === 'low_stock' && !a.is_acknowledged).length;
|
||||
const expiringItems = alerts.filter(a => a.alert_type === 'expiring_soon' && !a.is_acknowledged).length;
|
||||
const lowStockItems = 0;
|
||||
const expiringItems = 0;
|
||||
const totalValue = dashboardData?.total_value || 0;
|
||||
|
||||
return { totalItems, lowStockItems, expiringItems, totalValue };
|
||||
@@ -195,19 +179,6 @@ const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' })
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={() => setShowAlerts(!showAlerts)}
|
||||
className={`relative p-2 rounded-lg transition-colors ${
|
||||
showAlerts ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
{alerts.filter(a => !a.is_acknowledged).length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
||||
{alerts.filter(a => !a.is_acknowledged).length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => refresh()}
|
||||
@@ -230,7 +201,7 @@ const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' })
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Main Content */}
|
||||
<div className={`${showAlerts ? 'lg:col-span-3' : 'lg:col-span-4'}`}>
|
||||
<div className="lg:col-span-4">
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white p-4 rounded-lg border">
|
||||
@@ -534,17 +505,6 @@ const InventoryPage: React.FC<InventoryPageProps> = ({ view = 'stock-levels' })
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Alerts Panel */}
|
||||
{showAlerts && (
|
||||
<div className="lg:col-span-1">
|
||||
<StockAlertsPanel
|
||||
alerts={alerts}
|
||||
onAcknowledge={handleAcknowledgeAlert}
|
||||
onAcknowledgeAll={handleBulkAcknowledgeAlerts}
|
||||
onViewItem={handleViewItemById}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,18 +38,6 @@ export interface Forecast {
|
||||
features_used?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ForecastAlert {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
type: 'high_demand' | 'low_demand' | 'stockout_risk' | 'overproduction';
|
||||
inventory_product_id: string; // Reference to inventory service product
|
||||
product_name?: string; // Optional - for display, populated by frontend from inventory service
|
||||
message: string;
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
created_at: string;
|
||||
acknowledged: boolean;
|
||||
forecast_id?: string;
|
||||
}
|
||||
|
||||
export interface QuickForecast {
|
||||
product: string;
|
||||
@@ -63,12 +51,10 @@ export interface QuickForecast {
|
||||
interface ForecastState {
|
||||
forecasts: Forecast[];
|
||||
todayForecasts: QuickForecast[];
|
||||
alerts: ForecastAlert[];
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
isGeneratingForecast: boolean;
|
||||
isFetchingAlerts: boolean;
|
||||
|
||||
// Selected filters
|
||||
selectedDate: string;
|
||||
@@ -93,10 +79,8 @@ interface ForecastState {
|
||||
const initialState: ForecastState = {
|
||||
forecasts: [],
|
||||
todayForecasts: [],
|
||||
alerts: [],
|
||||
isLoading: false,
|
||||
isGeneratingForecast: false,
|
||||
isFetchingAlerts: false,
|
||||
selectedDate: new Date().toISOString().split('T')[0],
|
||||
selectedProduct: 'all',
|
||||
selectedLocation: '',
|
||||
@@ -215,40 +199,6 @@ export const fetchTodayForecasts = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchForecastAlerts = createAsyncThunk(
|
||||
'forecast/fetchAlerts',
|
||||
async (tenantId: string) => {
|
||||
const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/alerts`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch forecast alerts');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
);
|
||||
|
||||
export const acknowledgeAlert = createAsyncThunk(
|
||||
'forecast/acknowledgeAlert',
|
||||
async ({ tenantId, alertId }: { tenantId: string; alertId: string }) => {
|
||||
const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/alerts/${alertId}/acknowledge`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to acknowledge alert');
|
||||
}
|
||||
|
||||
return { alertId };
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchWeatherContext = createAsyncThunk(
|
||||
'forecast/fetchWeather',
|
||||
@@ -293,12 +243,6 @@ const forecastSlice = createSlice({
|
||||
setCurrentWeather: (state, action: PayloadAction<ForecastState['currentWeather']>) => {
|
||||
state.currentWeather = action.payload;
|
||||
},
|
||||
markAlertAsRead: (state, action: PayloadAction<string>) => {
|
||||
const alert = state.alerts.find(a => a.id === action.payload);
|
||||
if (alert) {
|
||||
alert.acknowledged = true;
|
||||
}
|
||||
},
|
||||
clearForecasts: (state) => {
|
||||
state.forecasts = [];
|
||||
state.todayForecasts = [];
|
||||
@@ -378,28 +322,6 @@ const forecastSlice = createSlice({
|
||||
state.error = action.error.message || 'Failed to fetch today\'s forecasts';
|
||||
})
|
||||
|
||||
// Fetch alerts
|
||||
builder
|
||||
.addCase(fetchForecastAlerts.pending, (state) => {
|
||||
state.isFetchingAlerts = true;
|
||||
})
|
||||
.addCase(fetchForecastAlerts.fulfilled, (state, action) => {
|
||||
state.isFetchingAlerts = false;
|
||||
state.alerts = action.payload.alerts || [];
|
||||
})
|
||||
.addCase(fetchForecastAlerts.rejected, (state, action) => {
|
||||
state.isFetchingAlerts = false;
|
||||
state.error = action.error.message || 'Failed to fetch alerts';
|
||||
})
|
||||
|
||||
// Acknowledge alert
|
||||
builder
|
||||
.addCase(acknowledgeAlert.fulfilled, (state, action) => {
|
||||
const alert = state.alerts.find(a => a.id === action.payload.alertId);
|
||||
if (alert) {
|
||||
alert.acknowledged = true;
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch weather context
|
||||
builder
|
||||
@@ -421,7 +343,6 @@ export const {
|
||||
addForecast,
|
||||
updateModelAccuracy,
|
||||
setCurrentWeather,
|
||||
markAlertAsRead,
|
||||
clearForecasts,
|
||||
} = forecastSlice.actions;
|
||||
|
||||
@@ -430,7 +351,6 @@ export default forecastSlice.reducer;
|
||||
// Selectors
|
||||
export const selectForecasts = (state: { forecast: ForecastState }) => state.forecast.forecasts;
|
||||
export const selectTodayForecasts = (state: { forecast: ForecastState }) => state.forecast.todayForecasts;
|
||||
export const selectForecastAlerts = (state: { forecast: ForecastState }) => state.forecast.alerts;
|
||||
export const selectForecastLoading = (state: { forecast: ForecastState }) => state.forecast.isLoading;
|
||||
export const selectForecastGenerating = (state: { forecast: ForecastState }) => state.forecast.isGeneratingForecast;
|
||||
export const selectForecastError = (state: { forecast: ForecastState }) => state.forecast.error;
|
||||
@@ -441,5 +361,3 @@ export const selectForecastFilters = (state: { forecast: ForecastState }) => ({
|
||||
});
|
||||
export const selectCurrentWeather = (state: { forecast: ForecastState }) => state.forecast.currentWeather;
|
||||
export const selectModelAccuracy = (state: { forecast: ForecastState }) => state.forecast.modelAccuracy;
|
||||
export const selectUnacknowledgedAlerts = (state: { forecast: ForecastState }) =>
|
||||
state.forecast.alerts.filter(alert => !alert.acknowledged);
|
||||
Reference in New Issue
Block a user