Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -8,19 +8,26 @@ import { PageHeader } from '../../../../components/layout';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { Equipment } from '../../../../api/types/equipment';
import { EquipmentModal } from '../../../../components/domain/equipment/EquipmentModal';
import { useEquipment, useCreateEquipment, useUpdateEquipment } from '../../../../api/hooks/equipment';
import { DeleteEquipmentModal } from '../../../../components/domain/equipment/DeleteEquipmentModal';
import { MaintenanceHistoryModal } from '../../../../components/domain/equipment/MaintenanceHistoryModal';
import { ScheduleMaintenanceModal, type MaintenanceScheduleData } from '../../../../components/domain/equipment/ScheduleMaintenanceModal';
import { useEquipment, useCreateEquipment, useUpdateEquipment, useDeleteEquipment, useHardDeleteEquipment } from '../../../../api/hooks/equipment';
const MaquinariaPage: React.FC = () => {
const { t } = useTranslation(['equipment', 'common']);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [selectedItem, setSelectedItem] = useState<Equipment | null>(null);
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
const [equipmentModalMode, setEquipmentModalMode] = useState<'view' | 'edit' | 'create'>('create');
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null);
// New modal states
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [equipmentForAction, setEquipmentForAction] = useState<Equipment | null>(null);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
@@ -29,9 +36,11 @@ const MaquinariaPage: React.FC = () => {
is_active: true
});
// Mutations for create and update
// Mutations for create, update, and delete
const createEquipmentMutation = useCreateEquipment(tenantId);
const updateEquipmentMutation = useUpdateEquipment(tenantId);
const deleteEquipmentMutation = useDeleteEquipment(tenantId);
const hardDeleteEquipmentMutation = useHardDeleteEquipment(tenantId);
const handleCreateEquipment = () => {
setSelectedEquipment({
@@ -73,19 +82,58 @@ const MaquinariaPage: React.FC = () => {
}
};
const handleScheduleMaintenance = (equipmentId: string) => {
console.log('Schedule maintenance for equipment:', equipmentId);
// Implementation would go here
const handleScheduleMaintenance = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowScheduleModal(true);
};
const handleAcknowledgeAlert = (equipmentId: string, alertId: string) => {
console.log('Acknowledge alert:', alertId, 'for equipment:', equipmentId);
// Implementation would go here
const handleScheduleMaintenanceSubmit = async (equipmentId: string, maintenanceData: MaintenanceScheduleData) => {
try {
// Update next maintenance date based on scheduled date
await updateEquipmentMutation.mutateAsync({
equipmentId: equipmentId,
equipmentData: {
nextMaintenance: maintenanceData.scheduledDate
} as Partial<Equipment>
});
setShowScheduleModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error scheduling maintenance:', error);
throw error;
}
};
const handleViewMaintenanceHistory = (equipmentId: string) => {
console.log('View maintenance history for equipment:', equipmentId);
// Implementation would go here
const handleViewMaintenanceHistory = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowHistoryModal(true);
};
const handleDeleteEquipment = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowDeleteModal(true);
};
const handleSoftDelete = async (equipmentId: string) => {
try {
await deleteEquipmentMutation.mutateAsync(equipmentId);
setShowDeleteModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error deleting equipment:', error);
throw error;
}
};
const handleHardDelete = async (equipmentId: string) => {
try {
await hardDeleteEquipmentMutation.mutateAsync(equipmentId);
setShowDeleteModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error hard deleting equipment:', error);
throw error;
}
};
const handleSaveEquipment = async (equipmentData: Equipment) => {
@@ -200,13 +248,9 @@ const MaquinariaPage: React.FC = () => {
];
const handleShowMaintenanceDetails = (equipment: Equipment) => {
setSelectedItem(equipment);
setShowMaintenanceModal(true);
};
const handleCloseMaintenanceModal = () => {
setShowMaintenanceModal(false);
setSelectedItem(null);
setSelectedEquipment(equipment);
setEquipmentModalMode('view');
setShowEquipmentModal(true);
};
// Loading state
@@ -336,23 +380,25 @@ const MaquinariaPage: React.FC = () => {
priority: 'primary',
onClick: () => handleShowMaintenanceDetails(equipment)
},
{
label: t('actions.edit'),
icon: Edit,
priority: 'secondary',
onClick: () => handleEditEquipment(equipment.id)
},
{
label: t('actions.view_history'),
icon: History,
priority: 'secondary',
onClick: () => handleViewMaintenanceHistory(equipment.id)
onClick: () => handleViewMaintenanceHistory(equipment)
},
{
label: t('actions.schedule_maintenance'),
icon: Wrench,
priority: 'secondary',
onClick: () => handleScheduleMaintenance(equipment.id)
highlighted: true,
onClick: () => handleScheduleMaintenance(equipment)
},
{
label: t('actions.delete'),
icon: Trash2,
priority: 'secondary',
destructive: true,
onClick: () => handleDeleteEquipment(equipment)
}
]}
/>
@@ -372,183 +418,7 @@ const MaquinariaPage: React.FC = () => {
/>
)}
{/* Maintenance Details Modal */}
{selectedItem && showMaintenanceModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div className="bg-[var(--bg-primary)] rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto my-8">
<div className="p-4 sm:p-6">
<div className="flex items-center justify-between mb-4 sm:mb-6">
<div>
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
{selectedItem.name}
</h2>
<p className="text-[var(--text-secondary)] text-sm">
{selectedItem.model} - {selectedItem.serialNumber}
</p>
</div>
<button
onClick={handleCloseMaintenanceModal}
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] p-1"
>
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4 sm:space-y-6">
{/* Equipment Status */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.status')}</h3>
<div className="flex items-center space-x-2">
<div
className="w-2 h-2 sm:w-3 sm:h-3 rounded-full"
style={{ backgroundColor: getStatusConfig(selectedItem.status).color }}
/>
<span className="text-[var(--text-primary)] text-sm sm:text-base">
{t(`equipment_status.${selectedItem.status}`)}
</span>
</div>
</div>
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.efficiency')}</h3>
<div className="text-lg sm:text-2xl font-bold text-[var(--text-primary)]">
{selectedItem.efficiency}%
</div>
</div>
</div>
{/* Maintenance Information */}
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.title')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.last')}</p>
<p className="text-[var(--text-primary)] text-sm sm:text-base">
{new Date(selectedItem.lastMaintenance).toLocaleDateString('es-ES')}
</p>
</div>
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.next')}</p>
<p className={`font-medium ${(new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime()) ? 'text-red-500' : 'text-[var(--text-primary)]'} text-sm sm:text-base`}>
{new Date(selectedItem.nextMaintenance).toLocaleDateString('es-ES')}
</p>
</div>
<div>
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.interval')}</p>
<p className="text-[var(--text-primary)] text-sm sm:text-base">
{selectedItem.maintenanceInterval} {t('common:units.days')}
</p>
</div>
</div>
{new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime() && (
<div className="mt-3 p-2 bg-red-50 dark:bg-red-900/20 rounded border-l-2 border-red-500">
<div className="flex items-center space-x-2">
<AlertTriangle className="w-4 h-4 text-red-500" />
<span className="text-xs sm:text-sm font-medium text-red-700 dark:text-red-300">
{t('maintenance.overdue')}
</span>
</div>
</div>
)}
</div>
{/* Active Alerts */}
{selectedItem.alerts.filter(a => !a.acknowledged).length > 0 && (
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('alerts.title')}</h3>
<div className="space-y-2 sm:space-y-3">
{selectedItem.alerts.filter(a => !a.acknowledged).map((alert) => (
<div
key={alert.id}
className={`p-2 sm:p-3 rounded border-l-2 ${
alert.type === 'critical' ? 'bg-red-50 border-red-500 dark:bg-red-900/20' :
alert.type === 'warning' ? 'bg-orange-50 border-orange-500 dark:bg-orange-900/20' :
'bg-blue-50 border-blue-500 dark:bg-blue-900/20'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<AlertTriangle className={`w-3 h-3 sm:w-4 sm:h-4 ${
alert.type === 'critical' ? 'text-red-500' :
alert.type === 'warning' ? 'text-orange-500' : 'text-blue-500'
}`} />
<span className="font-medium text-[var(--text-primary)] text-xs sm:text-sm">
{alert.message}
</span>
</div>
<span className="text-xs text-[var(--text-secondary)] hidden sm:block">
{new Date(alert.timestamp).toLocaleString('es-ES')}
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Maintenance History */}
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.history')}</h3>
<div className="space-y-3 sm:space-y-4">
{selectedItem.maintenanceHistory.map((history) => (
<div key={history.id} className="border-b border-[var(--border-primary)] pb-2 sm:pb-3 last:border-0 last:pb-0">
<div className="flex justify-between items-start">
<div>
<h4 className="font-medium text-[var(--text-primary)] text-sm">{history.description}</h4>
<p className="text-xs text-[var(--text-secondary)]">
{new Date(history.date).toLocaleDateString('es-ES')} - {history.technician}
</p>
</div>
<span className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
{t(`maintenance.type.${history.type}`)}
</span>
</div>
<div className="mt-1 sm:mt-2 flex flex-wrap gap-2">
<span className="text-xs">
<span className="text-[var(--text-secondary)]">{t('common:actions.cost')}:</span>
<span className="font-medium text-[var(--text-primary)]"> {history.cost}</span>
</span>
<span className="text-xs">
<span className="text-[var(--text-secondary)]">{t('fields.uptime')}:</span>
<span className="font-medium text-[var(--text-primary)]"> {history.downtime}h</span>
</span>
</div>
{history.partsUsed.length > 0 && (
<div className="mt-1 sm:mt-2">
<span className="text-xs text-[var(--text-secondary)]">{t('fields.parts')}:</span>
<div className="flex flex-wrap gap-1 mt-1">
{history.partsUsed.map((part, index) => (
<span key={index} className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
{part}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end space-x-2 sm:space-x-3 mt-4 sm:mt-6">
<Button variant="outline" size="sm" onClick={handleCloseMaintenanceModal}>
{t('common:actions.close')}
</Button>
<Button variant="primary" size="sm" onClick={() => selectedItem && handleScheduleMaintenance(selectedItem.id)}>
{t('actions.schedule_maintenance')}
</Button>
</div>
</div>
</div>
</div>
)}
{/* Equipment Modal */}
{/* Equipment Modal - Used for View Details, Edit, and Create */}
{showEquipmentModal && (
<EquipmentModal
isOpen={showEquipmentModal}
@@ -561,6 +431,47 @@ const MaquinariaPage: React.FC = () => {
mode={equipmentModalMode}
/>
)}
{/* Delete Equipment Modal */}
{showDeleteModal && equipmentForAction && (
<DeleteEquipmentModal
isOpen={showDeleteModal}
onClose={() => {
setShowDeleteModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSoftDelete={handleSoftDelete}
onHardDelete={handleHardDelete}
isLoading={deleteEquipmentMutation.isPending || hardDeleteEquipmentMutation.isPending}
/>
)}
{/* Maintenance History Modal */}
{showHistoryModal && equipmentForAction && (
<MaintenanceHistoryModal
isOpen={showHistoryModal}
onClose={() => {
setShowHistoryModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
/>
)}
{/* Schedule Maintenance Modal */}
{showScheduleModal && equipmentForAction && (
<ScheduleMaintenanceModal
isOpen={showScheduleModal}
onClose={() => {
setShowScheduleModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSchedule={handleScheduleMaintenanceSubmit}
isLoading={updateEquipmentMutation.isPending}
/>
)}
</div>
);
};

View File

@@ -187,7 +187,7 @@ const ProcurementPage: React.FC = () => {
const handleTriggerScheduler = async () => {
try {
await triggerSchedulerMutation.mutateAsync({ tenantId });
await triggerSchedulerMutation.mutateAsync(tenantId);
toast.success('Scheduler ejecutado exitosamente');
refetchPOs();
} catch (error) {

View File

@@ -1 +1 @@
export { default as ProcurementPage } from './ProcurementPage';
export { default as ProcurementPage } from './ProcurementPage';

View File

@@ -1,15 +1,16 @@
import React, { useState } from 'react';
import { Plus, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Euro, Loader, Trash2 } from 'lucide-react';
import { Button, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, AddModal, SearchAndFilter, DialogModal, type FilterConfig, EmptyState } from '../../../../components/ui';
import { Plus, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Euro, Loader, Trash2, DollarSign, Package } from 'lucide-react';
import { Button, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, AddModal, SearchAndFilter, DialogModal, Modal, ModalHeader, ModalBody, type FilterConfig, EmptyState } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { SupplierStatus, SupplierType, PaymentTerms } from '../../../../api/types/suppliers';
import { useSuppliers, useSupplierStatistics, useCreateSupplier, useUpdateSupplier, useApproveSupplier, useDeleteSupplier, useHardDeleteSupplier } from '../../../../api/hooks/suppliers';
import { useSuppliers, useSupplierStatistics, useCreateSupplier, useUpdateSupplier, useApproveSupplier, useDeleteSupplier, useHardDeleteSupplier, useSupplierPriceLists, useCreateSupplierPriceList, useUpdateSupplierPriceList, useDeleteSupplierPriceList } from '../../../../api/hooks/suppliers';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { useTranslation } from 'react-i18next';
import { statusColors } from '../../../../styles/colors';
import { DeleteSupplierModal } from '../../../../components/domain/suppliers';
import { DeleteSupplierModal, SupplierPriceListViewModal, PriceListModal } from '../../../../components/domain/suppliers';
import { useQueryClient } from '@tanstack/react-query';
const SuppliersPage: React.FC = () => {
const [activeTab] = useState('all');
@@ -23,6 +24,9 @@ const SuppliersPage: React.FC = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showApprovalModal, setShowApprovalModal] = useState(false);
const [supplierToApprove, setSupplierToApprove] = useState<any>(null);
const [showPriceListView, setShowPriceListView] = useState(false);
const [showAddPrice, setShowAddPrice] = useState(false);
const [priceListSupplier, setPriceListSupplier] = useState<any>(null);
// Get tenant ID from tenant store (preferred) or auth user (fallback)
const currentTenant = useCurrentTenant();
@@ -48,6 +52,7 @@ const SuppliersPage: React.FC = () => {
const suppliers = suppliersData || [];
const { t } = useTranslation(['suppliers', 'common']);
const queryClient = useQueryClient();
// Mutation hooks
const createSupplierMutation = useCreateSupplier();
@@ -56,6 +61,21 @@ const SuppliersPage: React.FC = () => {
const softDeleteMutation = useDeleteSupplier();
const hardDeleteMutation = useHardDeleteSupplier();
// Price list hooks
const {
data: priceListsData,
isLoading: priceListsLoading,
isRefetching: isRefetchingPriceLists
} = useSupplierPriceLists(
tenantId,
priceListSupplier?.id || '',
!!priceListSupplier?.id && showPriceListView
);
const createPriceListMutation = useCreateSupplierPriceList();
const updatePriceListMutation = useUpdateSupplierPriceList();
const deletePriceListMutation = useDeleteSupplierPriceList();
// Delete handlers
const handleSoftDelete = async (supplierId: string) => {
await softDeleteMutation.mutateAsync({ tenantId, supplierId });
@@ -65,6 +85,27 @@ const SuppliersPage: React.FC = () => {
return await hardDeleteMutation.mutateAsync({ tenantId, supplierId });
};
// Price list handlers
const handlePriceListSaveComplete = async () => {
if (!tenantId || !priceListSupplier?.id) return;
await queryClient.invalidateQueries({
queryKey: ['supplier-price-lists', tenantId, priceListSupplier.id]
});
};
const handleAddPriceSubmit = async (priceListData: any) => {
if (!priceListSupplier) return;
await createPriceListMutation.mutateAsync({
tenantId,
supplierId: priceListSupplier.id,
priceListData
});
// Close the add modal
setShowAddPrice(false);
};
const getSupplierStatusConfig = (status: SupplierStatus) => {
const statusConfig = {
[SupplierStatus.ACTIVE]: { text: t(`suppliers:status.${status.toLowerCase()}`), icon: CheckCircle },
@@ -274,6 +315,18 @@ const SuppliersPage: React.FC = () => {
setShowForm(true);
}
},
// Manage products action
{
label: t('suppliers:actions.manage_products'),
icon: Package,
variant: 'outline',
priority: 'secondary',
highlighted: true,
onClick: () => {
setPriceListSupplier(supplier);
setShowPriceListView(true);
}
},
// Approval action - Only show for pending suppliers + admin/super_admin
...(supplier.status === SupplierStatus.PENDING_APPROVAL &&
(user?.role === 'admin' || user?.role === 'super_admin')
@@ -769,7 +822,7 @@ const SuppliersPage: React.FC = () => {
placeholder: t('suppliers:placeholders.notes')
}
]
}] : [])
}] : []),
];
return (
@@ -942,6 +995,55 @@ const SuppliersPage: React.FC = () => {
}}
loading={approveSupplierMutation.isPending}
/>
{/* Price List View Modal */}
{priceListSupplier && (
<SupplierPriceListViewModal
isOpen={showPriceListView}
onClose={() => {
setShowPriceListView(false);
setPriceListSupplier(null);
}}
supplier={priceListSupplier}
priceLists={priceListsData || []}
loading={priceListsLoading}
tenantId={tenantId}
onAddPrice={() => setShowAddPrice(true)}
onEditPrice={async (priceId, updateData) => {
await updatePriceListMutation.mutateAsync({
tenantId,
supplierId: priceListSupplier.id,
priceListId: priceId,
priceListData: updateData
});
}}
onDeletePrice={async (priceId) => {
await deletePriceListMutation.mutateAsync({
tenantId,
supplierId: priceListSupplier.id,
priceListId: priceId
});
}}
waitForRefetch={true}
isRefetching={isRefetchingPriceLists}
onSaveComplete={handlePriceListSaveComplete}
/>
)}
{/* Add Price Modal */}
{priceListSupplier && (
<PriceListModal
isOpen={showAddPrice}
onClose={() => setShowAddPrice(false)}
onSave={handleAddPriceSubmit}
mode="create"
loading={createPriceListMutation.isPending}
excludeProductIds={priceListsData?.map(p => p.inventory_product_id) || []}
waitForRefetch={true}
isRefetching={isRefetchingPriceLists}
onSaveComplete={handlePriceListSaveComplete}
/>
)}
</div>
);
};