Add auth components, finance pages/hooks/services, contract components. Enhance LoginPage, AdminLayout, hooks. Remove legacy apiClient. Add mock data services for development. Addresses frontend gaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
/**
|
|
* PartidaModal - Modal para crear/editar partidas de contrato
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
useCreateContractPartida,
|
|
useUpdateContractPartida,
|
|
} from '../../hooks/useContracts';
|
|
import type {
|
|
ContractPartida,
|
|
CreateContractPartidaDto,
|
|
} from '../../types/contracts.types';
|
|
import {
|
|
Modal,
|
|
ModalFooter,
|
|
TextInput,
|
|
FormGroup,
|
|
} from '../common';
|
|
|
|
interface PartidaModalProps {
|
|
contractId: string;
|
|
partida: ContractPartida | null;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function PartidaModal({ contractId, partida, onClose }: PartidaModalProps) {
|
|
const createMutation = useCreateContractPartida();
|
|
const updateMutation = useUpdateContractPartida();
|
|
|
|
const [formData, setFormData] = useState<CreateContractPartidaDto & { conceptoCode?: string; conceptoDescription?: string }>({
|
|
conceptoId: partida?.conceptoId || '',
|
|
conceptoCode: partida?.conceptoCode || '',
|
|
conceptoDescription: partida?.conceptoDescription || '',
|
|
quantity: partida?.quantity || 0,
|
|
unitPrice: partida?.unitPrice || 0,
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
const payload: CreateContractPartidaDto = {
|
|
conceptoId: formData.conceptoId,
|
|
quantity: formData.quantity,
|
|
unitPrice: formData.unitPrice,
|
|
};
|
|
|
|
if (partida) {
|
|
await updateMutation.mutateAsync({
|
|
contractId,
|
|
partidaId: partida.id,
|
|
data: {
|
|
quantity: formData.quantity,
|
|
unitPrice: formData.unitPrice,
|
|
},
|
|
});
|
|
} else {
|
|
await createMutation.mutateAsync({ contractId, data: payload });
|
|
}
|
|
onClose();
|
|
};
|
|
|
|
const update = <K extends keyof typeof formData>(field: K, value: typeof formData[K]) => {
|
|
setFormData({ ...formData, [field]: value });
|
|
};
|
|
|
|
const isLoading = createMutation.isPending || updateMutation.isPending;
|
|
const total = formData.quantity * formData.unitPrice;
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat('es-MX', {
|
|
style: 'currency',
|
|
currency: 'MXN',
|
|
}).format(value);
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={true}
|
|
onClose={onClose}
|
|
title={partida ? 'Editar Partida' : 'Nueva Partida'}
|
|
size="md"
|
|
footer={
|
|
<ModalFooter>
|
|
<button
|
|
type="button"
|
|
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
onClick={onClose}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
form="partida-form"
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? 'Guardando...' : partida ? 'Guardar Cambios' : 'Agregar Partida'}
|
|
</button>
|
|
</ModalFooter>
|
|
}
|
|
>
|
|
<form id="partida-form" onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Concepto Selection - In a real app this would be a searchable select */}
|
|
<TextInput
|
|
label="ID del Concepto"
|
|
required
|
|
value={formData.conceptoId}
|
|
onChange={(e) => update('conceptoId', e.target.value)}
|
|
placeholder="UUID del concepto"
|
|
disabled={!!partida}
|
|
/>
|
|
|
|
{partida && (
|
|
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Concepto</p>
|
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
|
{partida.conceptoCode || 'N/A'}
|
|
</p>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
{partida.conceptoDescription || '-'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<FormGroup cols={2}>
|
|
<TextInput
|
|
label="Cantidad"
|
|
type="number"
|
|
required
|
|
value={formData.quantity.toString()}
|
|
onChange={(e) => update('quantity', parseFloat(e.target.value) || 0)}
|
|
placeholder="0.00"
|
|
min="0"
|
|
step="0.01"
|
|
/>
|
|
<TextInput
|
|
label="Precio Unitario"
|
|
type="number"
|
|
required
|
|
value={formData.unitPrice.toString()}
|
|
onChange={(e) => update('unitPrice', parseFloat(e.target.value) || 0)}
|
|
placeholder="0.00"
|
|
min="0"
|
|
step="0.01"
|
|
/>
|
|
</FormGroup>
|
|
|
|
{/* Total Calculated */}
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Total:
|
|
</span>
|
|
<span className="text-lg font-bold text-blue-600 dark:text-blue-400">
|
|
{formatCurrency(total)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
);
|
|
}
|