diff --git a/docker-compose.yml b/docker-compose.yml index bf7afeb..baf308c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,9 @@ services: - "5175:3000" environment: NODE_ENV: development + # Vite runs inside this container, so localhost would refer to the + # frontend rather than the Python service. + BACKEND_URL: http://backend:8000 AGENT_URL: http://backend:8000/api/v1/agent/agui depends_on: - backend diff --git a/frontend/src/components/SalaryEntryDialog.tsx b/frontend/src/components/SalaryEntryDialog.tsx new file mode 100644 index 0000000..d71993e --- /dev/null +++ b/frontend/src/components/SalaryEntryDialog.tsx @@ -0,0 +1,177 @@ +import { useState } from 'react'; +import { toast } from 'sonner'; + +import api from '@/lib/api'; +import { formatLocalDatetime } from '@/lib/format'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { AlertCircle } from 'lucide-react'; + +interface Props { + onClose: () => void; + onSaved: () => void; +} + +export default function SalaryEntryDialog({ onClose, onSaved }: Props) { + const [form, setForm] = useState({ + amount: '', + currency: 'CRC', + date: formatLocalDatetime(new Date()), + bank: 'BAC', + reference: '', + notes: '', + }); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const amount = Number(form.amount); + if (!Number.isFinite(amount) || amount <= 0) { + setError('Ingresá un monto mayor que cero.'); + return; + } + + setSaving(true); + setError(''); + try { + await api.post('/transactions/', { + amount, + currency: form.currency, + date: form.date, + bank: form.bank, + merchant: 'Salario', + transaction_type: 'SALARY', + source: 'TRANSFER', + reference: form.reference.trim() || null, + notes: form.notes.trim() || null, + }); + toast.success('Salario registrado'); + onSaved(); + onClose(); + } catch (err) { + const status = err && typeof err === 'object' && 'response' in err + ? (err as { response: { status: number } }).response.status + : null; + setError( + status === 409 + ? 'Ya existe una transacción con ese comprobante.' + : 'No se pudo registrar el salario. Intentá de nuevo.', + ); + } finally { + setSaving(false); + } + }; + + return ( + { if (!open) onClose(); }}> + + + Registrar salario + + Guardá el depósito recibido mientras configurás una nueva automatización. + + + +
+ {error && ( + + + {error} + + )} + +
+
+ + setForm({ ...form, amount: e.target.value })} + placeholder="0.00" + autoFocus + required + /> +
+
+ + +
+
+ + setForm({ ...form, date: e.target.value })} + required + /> +
+
+ + +
+
+ + setForm({ ...form, reference: e.target.value })} + placeholder="Opcional" + /> +
+
+ + setForm({ ...form, notes: e.target.value })} + placeholder="Ej. Quincena de julio" + /> +
+
+ + + + + +
+
+
+ ); +} diff --git a/frontend/src/pages/Salarios.tsx b/frontend/src/pages/Salarios.tsx index 2988c03..8024142 100644 --- a/frontend/src/pages/Salarios.tsx +++ b/frontend/src/pages/Salarios.tsx @@ -1,7 +1,7 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { type ColumnDef } from '@tanstack/react-table'; -import { Landmark, RefreshCw, Download } from 'lucide-react'; +import { Landmark, RefreshCw, Download, Plus } from 'lucide-react'; import { type Transaction, getSalarios, getSalariosSummary } from '@/lib/api'; import { formatAmount, formatDate } from '@/lib/format'; @@ -13,8 +13,10 @@ import { DataTableColumnHeader } from '@/components/ui/data-table-column-header' import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import SalaryEntryDialog from '@/components/SalaryEntryDialog'; export default function Salarios() { + const [entryOpen, setEntryOpen] = useState(false); const query = useQuery({ queryKey: ['salarios'], queryFn: async () => { @@ -95,6 +97,10 @@ export default function Salarios() { subtitle="Historial de depósitos salariales" actions={ <> +