diff --git a/backend/api/v1/modules/core/dashboard/__init__.py b/backend/api/v1/modules/core/dashboard/__init__.py new file mode 100644 index 00000000..2bf06e78 --- /dev/null +++ b/backend/api/v1/modules/core/dashboard/__init__.py @@ -0,0 +1,7 @@ +""" +Módulo de dashboard para estadísticas y métricas empresariales +""" + +from .routes import router + +__all__ = ["router"] diff --git a/backend/api/v1/modules/core/dashboard/dto.py b/backend/api/v1/modules/core/dashboard/dto.py new file mode 100644 index 00000000..acc981c6 --- /dev/null +++ b/backend/api/v1/modules/core/dashboard/dto.py @@ -0,0 +1,108 @@ +""" +DTOs para el dashboard de estadísticas y métricas empresariales +""" + +from typing import Dict, List, Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +class KPIMetric(BaseModel): + """Métrica individual de KPI""" + + label: str = Field(..., description="Nombre del indicador") + value: int | float = Field(..., description="Valor actual") + previous_value: Optional[int | float] = Field( + None, description="Valor anterior para comparación" + ) + percentage_change: Optional[float] = Field(None, description="Porcentaje de cambio") + trend: Optional[str] = Field(None, description="up, down, stable") + unit: Optional[str] = Field(None, description="Unidad de medida (%, USD, etc)") + + +class ActivityItem(BaseModel): + """Item de actividad reciente""" + + id: int + type: str = Field( + ..., description="Tipo de actividad: invoice, pedimento, client, etc" + ) + title: str = Field(..., description="Título descriptivo") + description: Optional[str] = Field(None, description="Descripción adicional") + timestamp: datetime + status: Optional[str] = Field(None, description="Estado del item") + icon: Optional[str] = Field(None, description="Icono a mostrar") + + +class ChartDataPoint(BaseModel): + """Punto de datos para gráficas""" + + label: str + value: float + category: Optional[str] = None + + +class DashboardStats(BaseModel): + """Estadísticas generales del dashboard""" + + # KPIs principales + total_invoices: KPIMetric + total_pedimentos: KPIMetric + total_clients: KPIMetric + total_providers: KPIMetric + active_items: KPIMetric + pending_approvals: KPIMetric + + # Estadísticas financieras + total_value_imports: Optional[float] = Field( + None, description="Valor total de importaciones" + ) + total_value_exports: Optional[float] = Field( + None, description="Valor total de exportaciones" + ) + + # Datos para gráficas + invoices_by_month: List[ChartDataPoint] = Field(default_factory=list) + operations_by_type: List[ChartDataPoint] = Field(default_factory=list) + top_clients: List[ChartDataPoint] = Field(default_factory=list) + top_providers: List[ChartDataPoint] = Field(default_factory=list) + + # Actividad reciente + recent_activity: List[ActivityItem] = Field(default_factory=list) + + # Metadata + generated_at: datetime = Field(default_factory=datetime.utcnow) + company_id: int + company_name: Optional[str] = None + + +class OperationsOverview(BaseModel): + """Vista general de operaciones""" + + total_operations: int + by_type: Dict[str, int] = Field(default_factory=dict) + by_status: Dict[str, int] = Field(default_factory=dict) + avg_processing_time: Optional[float] = Field( + None, description="Tiempo promedio en días" + ) + + +class InventoryMetrics(BaseModel): + """Métricas de inventario""" + + total_items: int + items_in_stock: int + items_low_stock: int + total_value: Optional[float] = None + by_category: Dict[str, int] = Field(default_factory=dict) + + +class ComplianceMetrics(BaseModel): + """Métricas de cumplimiento normativo""" + + pending_documents: int + expired_permits: int + upcoming_deadlines: int + compliance_score: Optional[float] = Field( + None, description="Score de cumplimiento 0-100" + ) diff --git a/backend/api/v1/modules/core/dashboard/routes.py b/backend/api/v1/modules/core/dashboard/routes.py new file mode 100644 index 00000000..6e48e78b --- /dev/null +++ b/backend/api/v1/modules/core/dashboard/routes.py @@ -0,0 +1,84 @@ +""" +Endpoints del dashboard para estadísticas y métricas empresariales +""" + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource + +from .dto import DashboardStats, OperationsOverview, InventoryMetrics +from .service import DashboardService + +router = APIRouter(prefix="/dashboard", tags=["Dashboard"]) + + +@router.get("/stats", response_model=DashboardStats) +async def get_dashboard_stats( + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Obtiene estadísticas completas del dashboard para la compañía especificada. + + Incluye: + - KPIs principales (facturas, pedimentos, clientes, proveedores, items) + - Gráficas de tendencias (facturas por mes, operaciones por tipo) + - Top clientes y proveedores + - Actividad reciente + """ + # Validar acceso + tenant_id = validate_access_to_resource(db, company_id, current_user) + + # Generar estadísticas + service = DashboardService(db, tenant_id, company_id) + stats = service.get_complete_dashboard_stats() + + return stats + + +@router.get("/operations-overview", response_model=OperationsOverview) +async def get_operations_overview( + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Obtiene una vista general de las operaciones + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + + service = DashboardService(db, tenant_id, company_id) + + # Implementación básica + ops_by_type = service.get_operations_by_type() + + return OperationsOverview( + total_operations=sum(int(op.value) for op in ops_by_type), + by_type={op.label: int(op.value) for op in ops_by_type}, + by_status={}, + ) + + +@router.get("/inventory-metrics", response_model=InventoryMetrics) +async def get_inventory_metrics( + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Obtiene métricas de inventario + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + + service = DashboardService(db, tenant_id, company_id) + items_kpi = service.get_items_metrics() + + return InventoryMetrics( + total_items=int(items_kpi.value), + items_in_stock=int(items_kpi.value), # Simplificado + items_low_stock=0, + by_category={}, + ) diff --git a/backend/api/v1/modules/core/dashboard/service.py b/backend/api/v1/modules/core/dashboard/service.py new file mode 100644 index 00000000..fdefad75 --- /dev/null +++ b/backend/api/v1/modules/core/dashboard/service.py @@ -0,0 +1,362 @@ +""" +Servicio para generar estadísticas y métricas del dashboard +""" + +from typing import List, Dict, Optional +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from sqlalchemy import func, and_, desc + +from api.v1.modules.a76.invoices.models import ( + InvoiceHeader, + InvoiceComplianceMx, + OperationType, +) +from api.v1.modules.a76.items.models import Item +from api.v1.modules.a76.clients_and_providers.models import ( + ClientProvider, + ClientOrProviderEnum, +) +from api.v1.modules.a76.general_catalogs.company.models import Company + +from .dto import ( + DashboardStats, + KPIMetric, + ActivityItem, + ChartDataPoint, + OperationsOverview, + InventoryMetrics, +) + + +class DashboardService: + """Servicio para generar estadísticas del dashboard""" + + def __init__(self, db: Session, tenant_id: int, company_id: int): + self.db = db + self.tenant_id = tenant_id + self.company_id = company_id + + def _calculate_trend(self, current: int, previous: int) -> tuple[float, str]: + """Calcula el cambio porcentual y la tendencia""" + if previous == 0: + return 0.0, "stable" if current == 0 else "up" + + change = ((current - previous) / previous) * 100 + + if abs(change) < 1: + trend = "stable" + elif change > 0: + trend = "up" + else: + trend = "down" + + return round(change, 2), trend + + def get_invoice_metrics(self) -> tuple[KPIMetric, List[ChartDataPoint]]: + """Obtiene métricas de facturas""" + # Total de facturas actuales + current_total = ( + self.db.query(func.count(InvoiceHeader.id)) + .filter( + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + ) + .scalar() + or 0 + ) + + # Total del mes anterior para comparación + last_month = datetime.utcnow() - timedelta(days=30) + previous_total = ( + self.db.query(func.count(InvoiceHeader.id)) + .filter( + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + InvoiceHeader.created_at < last_month, + ) + .scalar() + or 0 + ) + + percentage, trend = self._calculate_trend(current_total, previous_total) + + kpi = KPIMetric( + label="Total Facturas", + value=current_total, + previous_value=previous_total, + percentage_change=percentage, + trend=trend, + ) + + # Facturas por mes (últimos 6 meses) + six_months_ago = datetime.utcnow() - timedelta(days=180) + + monthly_data = ( + self.db.query( + func.date_trunc("month", InvoiceHeader.created_at).label("month"), + func.count(InvoiceHeader.id).label("count"), + ) + .filter( + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + InvoiceHeader.created_at >= six_months_ago, + ) + .group_by("month") + .order_by("month") + .all() + ) + + chart_data = [ + ChartDataPoint( + label=row.month.strftime("%b %Y") if row.month else "Unknown", + value=float(row.count), + ) + for row in monthly_data + ] + + return kpi, chart_data + + def get_client_provider_metrics( + self, + ) -> tuple[KPIMetric, KPIMetric, List[ChartDataPoint], List[ChartDataPoint]]: + """Obtiene métricas de clientes y proveedores""" + # Total clientes + total_clients = ( + self.db.query(func.count(ClientProvider.id)) + .filter( + ClientProvider.tenant_id == self.tenant_id, + ClientProvider.company_id == self.company_id, + ClientProvider.client_or_provider.in_( + [ClientOrProviderEnum.CLIENT, ClientOrProviderEnum.BOTH] + ), + ) + .scalar() + or 0 + ) + + # Total proveedores + total_providers = ( + self.db.query(func.count(ClientProvider.id)) + .filter( + ClientProvider.tenant_id == self.tenant_id, + ClientProvider.company_id == self.company_id, + ClientProvider.client_or_provider.in_( + [ClientOrProviderEnum.PROVIDER, ClientOrProviderEnum.BOTH] + ), + ) + .scalar() + or 0 + ) + + client_kpi = KPIMetric( + label="Total Clientes", value=total_clients, trend="stable" + ) + + provider_kpi = KPIMetric( + label="Total Proveedores", value=total_providers, trend="stable" + ) + + # Top 5 clientes por número de facturas + # Los clientes están en InvoiceComplianceMx.sold_to_id + top_clients = ( + self.db.query( + ClientProvider.name, func.count(InvoiceHeader.id).label("invoice_count") + ) + .join( + InvoiceComplianceMx, + InvoiceComplianceMx.sold_to_id == ClientProvider.id, + isouter=True, + ) + .join( + InvoiceHeader, + and_( + InvoiceHeader.id == InvoiceComplianceMx.invoice_id, + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + ), + isouter=True, + ) + .filter( + ClientProvider.tenant_id == self.tenant_id, + ClientProvider.company_id == self.company_id, + ClientProvider.client_or_provider.in_( + [ClientOrProviderEnum.CLIENT, ClientOrProviderEnum.BOTH] + ), + ) + .group_by(ClientProvider.id, ClientProvider.name) + .order_by(desc("invoice_count")) + .limit(5) + .all() + ) + + top_clients_data = [ + ChartDataPoint( + label=row.name or f"Cliente {i+1}", value=float(row.invoice_count or 0) + ) + for i, row in enumerate(top_clients) + ] + + # Top 5 proveedores + # Los proveedores están en InvoiceComplianceMx.provider_id + top_providers = ( + self.db.query( + ClientProvider.name, func.count(InvoiceHeader.id).label("invoice_count") + ) + .join( + InvoiceComplianceMx, + InvoiceComplianceMx.provider_id == ClientProvider.id, + isouter=True, + ) + .join( + InvoiceHeader, + and_( + InvoiceHeader.id == InvoiceComplianceMx.invoice_id, + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + ), + isouter=True, + ) + .filter( + ClientProvider.tenant_id == self.tenant_id, + ClientProvider.company_id == self.company_id, + ClientProvider.client_or_provider.in_( + [ClientOrProviderEnum.PROVIDER, ClientOrProviderEnum.BOTH] + ), + ) + .group_by(ClientProvider.id, ClientProvider.name) + .order_by(desc("invoice_count")) + .limit(5) + .all() + ) + + top_providers_data = [ + ChartDataPoint( + label=row.name or f"Proveedor {i+1}", + value=float(row.invoice_count or 0), + ) + for i, row in enumerate(top_providers) + ] + + return client_kpi, provider_kpi, top_clients_data, top_providers_data + + def get_items_metrics(self) -> KPIMetric: + """Obtiene métricas de items/productos""" + total_items = ( + self.db.query(func.count(Item.id)) + .filter( + Item.tenant_id == self.tenant_id, Item.company_id == self.company_id + ) + .scalar() + or 0 + ) + + return KPIMetric(label="Total Items", value=total_items, trend="stable") + + def get_operations_by_type(self) -> List[ChartDataPoint]: + """Obtiene distribución de operaciones por tipo""" + ops_data = ( + self.db.query( + InvoiceHeader.operation_type, + func.count(InvoiceHeader.id).label("count"), + ) + .filter( + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + ) + .group_by(InvoiceHeader.operation_type) + .all() + ) + + type_labels = { + OperationType.IMP: "Importación", + OperationType.EXP: "Exportación", + OperationType.SM_IN: "Entrada SM", + OperationType.SM_OUT: "Salida SM", + OperationType.CTM_SEND: "Envío CTM", + OperationType.CTM_RECEIVE: "Recibo CTM", + } + + return [ + ChartDataPoint( + label=type_labels.get(row.operation_type, str(row.operation_type)), + value=float(row.count), + ) + for row in ops_data + if row.operation_type + ] + + def get_recent_activity(self, limit: int = 10) -> List[ActivityItem]: + """Obtiene actividad reciente del sistema""" + activities = [] + + # Facturas recientes + recent_invoices = ( + self.db.query(InvoiceHeader) + .filter( + InvoiceHeader.tenant_id == self.tenant_id, + InvoiceHeader.company_id == self.company_id, + ) + .order_by(desc(InvoiceHeader.created_at)) + .limit(limit) + .all() + ) + + for invoice in recent_invoices: + activities.append( + ActivityItem( + id=invoice.id, + type="invoice", + title=f"Factura {invoice.invoice_number or invoice.id}", + description=f"Operación: {invoice.operation_type if invoice.operation_type else 'N/A'}", + timestamp=invoice.created_at, + status="active", + icon="FileText", + ) + ) + + # Ordenar por timestamp + activities.sort(key=lambda x: x.timestamp, reverse=True) + + return activities[:limit] + + def get_complete_dashboard_stats(self) -> DashboardStats: + """Genera estadísticas completas para el dashboard""" + + # Obtener nombre de la compañía + company = ( + self.db.query(Company) + .filter(Company.id == self.company_id, Company.tenant_id == self.tenant_id) + .first() + ) + + # Obtener todas las métricas + invoice_kpi, invoices_chart = self.get_invoice_metrics() + client_kpi, provider_kpi, top_clients, top_providers = ( + self.get_client_provider_metrics() + ) + items_kpi = self.get_items_metrics() + operations_chart = self.get_operations_by_type() + recent_activity = self.get_recent_activity() + + # KPI de pedimentos (placeholder - implementar cuando exista el modelo) + pedimento_kpi = KPIMetric(label="Total Pedimentos", value=0, trend="stable") + + # KPI de aprobaciones pendientes + pending_kpi = KPIMetric(label="Pendientes", value=0, trend="stable") + + return DashboardStats( + total_invoices=invoice_kpi, + total_pedimentos=pedimento_kpi, + total_clients=client_kpi, + total_providers=provider_kpi, + active_items=items_kpi, + pending_approvals=pending_kpi, + invoices_by_month=invoices_chart, + operations_by_type=operations_chart, + top_clients=top_clients, + top_providers=top_providers, + recent_activity=recent_activity, + company_id=self.company_id, + company_name=company.name if company else None, + ) diff --git a/backend/api/v1/modules/core/router.py b/backend/api/v1/modules/core/router.py index 66b5b0d2..53e412ef 100644 --- a/backend/api/v1/modules/core/router.py +++ b/backend/api/v1/modules/core/router.py @@ -2,6 +2,7 @@ from .auth.routes import router as auth_router from .licenses.routes import router as licenses_router from .tenants.routes import router as tenants_router from .user_tenant.routes import router as user_tenant_router +from .dashboard.routes import router as dashboard_router from fastapi import APIRouter router = APIRouter() @@ -9,4 +10,5 @@ router = APIRouter() router.include_router(auth_router) router.include_router(tenants_router, prefix="/core", tags=["core / tenants"]) router.include_router(user_tenant_router, prefix="/core", tags=["core / user-tenants"]) -router.include_router(licenses_router, prefix="/core", tags=["core / licenses"]) \ No newline at end of file +router.include_router(licenses_router, prefix="/core", tags=["core / licenses"]) +router.include_router(dashboard_router, prefix="/core", tags=["core / dashboard"]) diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-measures.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-measures.ts index 3f233a34..34f42a36 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-measures.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-measures.ts @@ -64,12 +64,12 @@ export async function createUnitMeasure(data: UnitMeasureCreate): Promise> { - return await api.put(`/a76/units-of-measure/${id}`, data); + return await api.put(`/a76/units-of-measure/${id}/`, data); } /** * Elimina una unidad de medida */ export async function deleteUnitMeasure(id: number): Promise> { - return await api.delete(`/a76/units-of-measure/${id}`); + return await api.delete(`/a76/units-of-measure/${id}/`); } diff --git a/frontend/src/lib/api/dashboard/a76/units_of_measure.ts b/frontend/src/lib/api/dashboard/a76/units_of_measure.ts index cbfce30e..5aa0b796 100644 --- a/frontend/src/lib/api/dashboard/a76/units_of_measure.ts +++ b/frontend/src/lib/api/dashboard/a76/units_of_measure.ts @@ -59,7 +59,7 @@ export const unitsOfMeasureApi = { page_size: pageSize.toString() }); return api.get( - `/v1/a76/units-of-measure?${params.toString()}` + `/v1/a76/units-of-measure/?${params.toString()}` ); }, @@ -77,7 +77,7 @@ export const unitsOfMeasureApi = { * @param data - Datos de la unidad de medida a crear */ create: (companyId: number, data: CreateUnitOfMeasureData) => - api.post(`/v1/a76/units-of-measure?company_id=${companyId}`, data), + api.post(`/v1/a76/units-of-measure/?company_id=${companyId}`, data), /** * Actualiza una unidad de medida existente @@ -87,7 +87,7 @@ export const unitsOfMeasureApi = { */ update: (companyId: number, id: number, data: UpdateUnitOfMeasureData) => api.put( - `/v1/a76/units-of-measure/${id}?company_id=${companyId}`, + `/v1/a76/units-of-measure/${id}/?company_id=${companyId}`, data ), @@ -97,5 +97,5 @@ export const unitsOfMeasureApi = { * @param id - ID de la unidad de medida a eliminar */ delete: (companyId: number, id: number) => - api.delete(`/v1/a76/units-of-measure/${id}?company_id=${companyId}`) + api.delete(`/v1/a76/units-of-measure/${id}/?company_id=${companyId}`) }; diff --git a/frontend/src/lib/api/dashboard/index.ts b/frontend/src/lib/api/dashboard/index.ts new file mode 100644 index 00000000..0c02a29f --- /dev/null +++ b/frontend/src/lib/api/dashboard/index.ts @@ -0,0 +1,42 @@ +/** + * Cliente API para el dashboard + */ + +import { api } from '$lib/api'; +import type { DashboardStats, OperationsOverview, InventoryMetrics } from './types'; + +export async function getDashboardStats(companyId: number): Promise { + const response = await api.get( + `/v1/core/dashboard/stats?company_id=${companyId}` + ); + + if (response.error || !response.data) { + throw new Error(response.error || 'Failed to fetch dashboard stats'); + } + + return response.data; +} + +export async function getOperationsOverview(companyId: number): Promise { + const response = await api.get( + `/v1/core/dashboard/operations-overview?company_id=${companyId}` + ); + + if (response.error || !response.data) { + throw new Error(response.error || 'Failed to fetch operations overview'); + } + + return response.data; +} + +export async function getInventoryMetrics(companyId: number): Promise { + const response = await api.get( + `/v1/core/dashboard/inventory-metrics?company_id=${companyId}` + ); + + if (response.error || !response.data) { + throw new Error(response.error || 'Failed to fetch inventory metrics'); + } + + return response.data; +} diff --git a/frontend/src/lib/api/dashboard/types.ts b/frontend/src/lib/api/dashboard/types.ts new file mode 100644 index 00000000..52088235 --- /dev/null +++ b/frontend/src/lib/api/dashboard/types.ts @@ -0,0 +1,62 @@ +/** + * Tipos TypeScript para el dashboard + */ + +export interface KPIMetric { + label: string; + value: number; + previous_value?: number; + percentage_change?: number; + trend?: 'up' | 'down' | 'stable'; + unit?: string; +} + +export interface ActivityItem { + id: number; + type: string; + title: string; + description?: string; + timestamp: string; + status?: string; + icon?: string; +} + +export interface ChartDataPoint { + label: string; + value: number; + category?: string; +} + +export interface DashboardStats { + total_invoices: KPIMetric; + total_pedimentos: KPIMetric; + total_clients: KPIMetric; + total_providers: KPIMetric; + active_items: KPIMetric; + pending_approvals: KPIMetric; + total_value_imports?: number; + total_value_exports?: number; + invoices_by_month: ChartDataPoint[]; + operations_by_type: ChartDataPoint[]; + top_clients: ChartDataPoint[]; + top_providers: ChartDataPoint[]; + recent_activity: ActivityItem[]; + generated_at: string; + company_id: number; + company_name?: string; +} + +export interface OperationsOverview { + total_operations: number; + by_type: Record; + by_status: Record; + avg_processing_time?: number; +} + +export interface InventoryMetrics { + total_items: number; + items_in_stock: number; + items_low_stock: number; + total_value?: number; + by_category: Record; +} diff --git a/frontend/src/lib/components/dashboard/activity-feed.svelte b/frontend/src/lib/components/dashboard/activity-feed.svelte new file mode 100644 index 00000000..fde3b9b4 --- /dev/null +++ b/frontend/src/lib/components/dashboard/activity-feed.svelte @@ -0,0 +1,106 @@ + + + + + Actividad Reciente + Últimas operaciones registradas en el sistema + + + {#if activities.length === 0} +
+ +

No hay actividad reciente

+
+ {:else} +
+ {#each activities as activity} + {@const Icon = getIcon(activity.type)} +
+
+
+ +
+
+
+
+

{activity.title}

+ + {formatDate(activity.timestamp)} + +
+ {#if activity.description} +

{activity.description}

+ {/if} + {#if activity.status} + + {activity.status} + + {/if} +
+
+ {/each} +
+ {/if} +
+
diff --git a/frontend/src/lib/components/dashboard/chart-card.svelte b/frontend/src/lib/components/dashboard/chart-card.svelte new file mode 100644 index 00000000..529f10bb --- /dev/null +++ b/frontend/src/lib/components/dashboard/chart-card.svelte @@ -0,0 +1,59 @@ + + + + + {title} + {#if description} + {description} + {/if} + + + {#if data.length === 0} +
No hay datos disponibles
+ {:else if type === 'bar'} +
+ {#each data as item} +
+
+ {item.label} + {item.value.toLocaleString()} +
+
+
+
+
+ {/each} +
+ {:else if type === 'pie'} +
+ {#each data as item} +
+
+
+
{item.label}
+
{item.value.toLocaleString()}
+
+
+ {/each} +
+ {/if} +
+
diff --git a/frontend/src/lib/components/dashboard/donut-chart.svelte b/frontend/src/lib/components/dashboard/donut-chart.svelte new file mode 100644 index 00000000..64f6c17f --- /dev/null +++ b/frontend/src/lib/components/dashboard/donut-chart.svelte @@ -0,0 +1,104 @@ + + + + + {title} + + + {#if data.length === 0} +
No hay datos disponibles
+ {:else} +
+ +
+ + {#each donutSegments() as segment, i} + + {/each} + + +
+
+
{total.toLocaleString()}
+
Total
+
+
+
+ + +
+ {#each segments as segment, i} +
+
+
+ {segment.label} +
+
+ + {segment.value.toLocaleString()} + + + ({segment.percentage.toFixed(1)}%) + +
+
+ {/each} +
+
+ {/if} +
+
diff --git a/frontend/src/lib/components/dashboard/kpi-card.svelte b/frontend/src/lib/components/dashboard/kpi-card.svelte new file mode 100644 index 00000000..0c0db870 --- /dev/null +++ b/frontend/src/lib/components/dashboard/kpi-card.svelte @@ -0,0 +1,56 @@ + + + + + + {metric.label} + + {#if Icon} + + {/if} + + +
+ {metric.value.toLocaleString()} + {#if metric.unit} + {metric.unit} + {/if} +
+ {#if metric.percentage_change !== undefined && TrendIcon} +
+ + + {Math.abs(metric.percentage_change).toFixed(1)}% + + vs mes anterior +
+ {/if} +
+
diff --git a/frontend/src/lib/components/dashboard/trend-chart.svelte b/frontend/src/lib/components/dashboard/trend-chart.svelte new file mode 100644 index 00000000..e9dd978e --- /dev/null +++ b/frontend/src/lib/components/dashboard/trend-chart.svelte @@ -0,0 +1,73 @@ + + + + + Tendencia de Operaciones + Evolución mensual de facturas y pedimentos + + + {#if monthlyData.length === 0} +
+

No hay datos disponibles

+
+ {:else} + +
+ {#each monthlyData as point, i} +
+ +
+
+ +
+ {point.value.toLocaleString()} +
+
+
+ + {point.label} +
+ {/each} +
+ + +
+
+
+ {monthlyData.reduce((sum, d) => sum + d.value, 0).toLocaleString()} +
+
Total
+
+
+
+ {Math.round( + monthlyData.reduce((sum, d) => sum + d.value, 0) / monthlyData.length + ).toLocaleString()} +
+
Promedio
+
+
+
{maxValue.toLocaleString()}
+
Máximo
+
+
+ {/if} +
+
diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 71473c62..1c81c69d 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -1,79 +1,217 @@
- +
-

Bienvenido al Dashboard

-

- Sistema de gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT -

-
- - -
- - - Total de Pedimentos - - -
0
-

Registros activos

-
-
- - - - Datos de Referencia - - -
12
-

Catálogos disponibles

-
-
- - - - Licencia Activa - - -
-

Cuenta verificada

-
-
-
- - - - - Accesos Rápidos - Accede a las funciones más utilizadas del sistema - - -
- - - Código Pedimento - Regímenes - Gestionar relaciones -
- - Catálogo de Tipos - Próximamente -
- - Reportes - Próximamente +
+
+

Dashboard

+

+ {#if stats?.company_name} + {stats.company_name} - Sistema de gestión de comercio exterior + {:else} + Sistema de gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT + {/if} +

+
- - -
\ No newline at end of file +
+ + {#if error} + + + Error + {error} + + {/if} + + {#if loading} +
+ {#each Array(6) as _} + + +
+
+ +
+
+
+ {/each} +
+ {:else if stats} + +
+ + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + +
+
+
+ {stats.total_invoices.value + stats.total_pedimentos.value} +
+
Total de Documentos
+
+
+
+ {stats.total_clients.value + stats.total_providers.value} +
+
Total de Contactos
+
+
+
{stats.active_items.value}
+
Items Activos
+
+
+
+
+ {/if} +