Initial commit: proyecto ContabilidadSaPolar completo

This commit is contained in:
root
2026-08-19 21:30:23 +00:00
commit 0d5c6f9512
232 changed files with 26730 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
import { useEffect, useState, useMemo } from 'react';
import { bankAccountApi } from '../api/resources';
import type { BankAccount } from '../types/api';
import ConfirmDialog from '../components/ConfirmDialog';
import { useToast } from '../components/Toast';
import SortableHeader from '../components/SortableHeader';
import { useSort } from '../hooks/useSort';
type ViewMode = 'list' | 'detail' | 'edit' | 'create';
interface FormData {
name: string;
holder: string;
iban: string;
bankName: string;
swiftBic: string;
currency: string;
isDefault: boolean;
description: string;
}
const emptyForm = (): FormData => ({
name: '', holder: '', iban: '', bankName: '', swiftBic: '',
currency: 'EUR', isDefault: false, description: '',
});
function formatIban(iban: string): string {
const cleaned = iban.replace(/\s+/g, '').toUpperCase();
return cleaned.replace(/(.{4})/g, '$1 ').trim();
}
export default function BankAccounts() {
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [items, setItems] = useState<BankAccount[]>([]);
const [selected, setSelected] = useState<BankAccount | null>(null);
const [form, setForm] = useState<FormData>(emptyForm());
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<BankAccount | null>(null);
const { toast } = useToast();
const { sorted, sort, toggleSort } = useSort(items, 'name');
// -- Bank name autocomplete options --
const bankNames = useMemo(() => {
const names = new Set<string>();
items.forEach((item) => {
if (item.bankName) names.add(item.bankName);
});
return [...names].sort();
}, [items]);
const loadList = () => {
setLoading(true);
bankAccountApi.getAll({ all: true })
.then((resp) => setItems(resp.data))
.catch(() => toast('Error al cargar cuentas bancarias', 'error'))
.finally(() => setLoading(false));
};
useEffect(() => { loadList(); }, []);
const goToDetail = (item: BankAccount) => {
setSelected(item);
setViewMode('detail');
};
const goToCreate = () => {
setSelected(null);
setForm(emptyForm());
setViewMode('create');
};
const goToEdit = (item: BankAccount) => {
setSelected(item);
setForm({
name: item.name,
holder: item.holder,
iban: item.iban,
bankName: item.bankName ?? '',
swiftBic: item.swiftBic ?? '',
currency: item.currency,
isDefault: item.isDefault,
description: item.description ?? '',
});
setViewMode('edit');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.name.trim() || !form.holder.trim() || !form.iban.trim()) {
toast('Completa los campos obligatorios', 'warning');
return;
}
setSaving(true);
try {
const payload: Record<string, unknown> = {
name: form.name,
holder: form.holder,
iban: form.iban.replace(/\s+/g, '').toUpperCase(),
bankName: form.bankName || null,
swiftBic: form.swiftBic || null,
currency: form.currency,
isDefault: form.isDefault,
description: form.description || null,
};
if (viewMode === 'edit' && selected) {
await bankAccountApi.update(selected.id, payload as Partial<BankAccount>);
toast('Cuenta actualizada', 'success');
} else {
await bankAccountApi.create(payload as Partial<BankAccount>);
toast('Cuenta creada', 'success');
}
setViewMode('list');
loadList();
} catch {
toast('Error al guardar la cuenta', 'error');
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!deleteTarget) return;
try {
await bankAccountApi.remove(deleteTarget.id);
toast('Cuenta desactivada', 'success');
setDeleteTarget(null);
loadList();
if (viewMode === 'detail') setViewMode('list');
} catch {
toast('Error al eliminar la cuenta', 'error');
}
};
if (viewMode === 'list') {
return (
<>
<div className="page-header">
<div>
<h1>Cuentas Bancarias</h1>
<p className="text-muted">Gestiona las cuentas bancarias de la empresa</p>
</div>
<button className="btn btn-primary" onClick={goToCreate}>+ Nueva Cuenta</button>
</div>
<div className="card">
{loading ? (
<div className="loading"><div className="spinner" /> Cargando...</div>
) : items.length === 0 ? (
<div className="empty-state">
<h3>No hay cuentas bancarias</h3>
<p>Añade una cuenta bancaria para asociarla a gastos e ingresos.</p>
</div>
) : (
<div className="table-container">
<div className="table-wrapper">
<table className="data-table">
<thead>
<tr>
<SortableHeader label="Nombre" sortKey="name" sort={sort} onToggle={toggleSort} />
<SortableHeader label="Banco" sortKey="bankName" sort={sort} onToggle={toggleSort} />
<SortableHeader label="IBAN" sortKey="iban" sort={sort} onToggle={toggleSort} />
<SortableHeader label="Titular" sortKey="holder" sort={sort} onToggle={toggleSort} />
<SortableHeader label="Divisa" sortKey="currency" sort={sort} onToggle={toggleSort} />
<SortableHeader label="Estado" sortKey="active" sort={sort} onToggle={toggleSort} />
<th>Acciones</th>
</tr>
</thead>
<tbody>
{sorted.map((acc) => (
<tr key={acc.id}>
<td className="font-semibold">
{acc.name}
{acc.isDefault && <span className="badge badge-primary" style={{ marginLeft: 8 }}>Ppal</span>}
</td>
<td className="text-secondary">{acc.bankName ?? '-'}</td>
<td className="font-mono" style={{ fontSize: '0.85em' }}>{formatIban(acc.iban)}</td>
<td className="text-secondary">{acc.holder}</td>
<td>{acc.currency}</td>
<td>{acc.active ? <span className="badge badge-success"><span className="badge-dot badge-dot-success" /> Activa</span> : <span className="badge badge-danger"><span className="badge-dot badge-dot-danger" /> Inactiva</span>}</td>
<td>
<div className="actions">
<button className="btn btn-sm btn-secondary" onClick={() => goToDetail(acc)}>Ver</button>
<button className="btn btn-sm btn-secondary" onClick={() => goToEdit(acc)}>Editar</button>
{acc.active && (
<button className="btn btn-sm btn-danger" onClick={() => setDeleteTarget(acc)}>Desactivar</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
<ConfirmDialog
open={!!deleteTarget}
title="Desactivar Cuenta"
message={`¿Estás seguro de desactivar "${deleteTarget?.name}"? Los gastos e ingresos asociados no se verán afectados.`}
onConfirm={handleDelete}
onCancel={() => setDeleteTarget(null)}
/>
</>
);
}
if (viewMode === 'detail' && selected) {
return (
<>
<div className="page-header">
<div>
<h1>{selected.name}</h1>
</div>
<div className="actions">
<button className="btn btn-secondary" onClick={() => setViewMode('list')}>Volver</button>
<button className="btn btn-primary" onClick={() => goToEdit(selected)}>Editar</button>
</div>
</div>
<div className="card">
<div className="detail-grid">
<div className="detail-field">
<span className="detail-label">Nombre</span>
<span className="detail-value">{selected.name}</span>
</div>
<div className="detail-field">
<span className="detail-label">Banco</span>
<span className="detail-value">{selected.bankName ?? '-'}</span>
</div>
<div className="detail-field">
<span className="detail-label">IBAN</span>
<span className="detail-value font-mono">{formatIban(selected.iban)}</span>
</div>
<div className="detail-field">
<span className="detail-label">SWIFT/BIC</span>
<span className="detail-value font-mono">{selected.swiftBic ?? '-'}</span>
</div>
<div className="detail-field">
<span className="detail-label">Titular</span>
<span className="detail-value">{selected.holder}</span>
</div>
<div className="detail-field">
<span className="detail-label">Divisa</span>
<span className="detail-value">{selected.currency}</span>
</div>
<div className="detail-field">
<span className="detail-label">Por Defecto</span>
<span className="detail-value">{selected.isDefault ? 'Sí' : 'No'}</span>
</div>
<div className="detail-field">
<span className="detail-label">Estado</span>
<span className="detail-value">{selected.active ? <span className="badge badge-success"><span className="badge-dot badge-dot-success" /> Activa</span> : <span className="badge badge-danger"><span className="badge-dot badge-dot-danger" /> Inactiva</span>}</span>
</div>
{selected.description && (
<div className="detail-field full-width">
<span className="detail-label">Descripción</span>
<span className="detail-value">{selected.description}</span>
</div>
)}
</div>
</div>
</>
);
}
// Create / Edit form
const isEdit = viewMode === 'edit';
return (
<>
<div className="page-header">
<h1>{isEdit ? `Editar: ${selected?.name}` : 'Nueva Cuenta Bancaria'}</h1>
</div>
<div className="form-sticky-bar">
<button className="btn btn-secondary" onClick={() => setViewMode(isEdit && selected ? 'detail' : 'list')}> Cancelar</button>
<button type="submit" className="btn btn-primary" disabled={saving} form="bank-account-form">
{saving ? 'Guardando...' : (isEdit ? 'Actualizar' : 'Crear')}
</button>
</div>
<div className="card" style={{ marginTop: '24px' }}>
<form id="bank-account-form" onSubmit={handleSubmit} className="form">
<div className="form-grid">
<div className="form-group">
<label className="form-label">Nombre *</label>
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required placeholder="Ej: BBVA Empresas" />
</div>
<div className="form-group">
<label className="form-label">Titular *</label>
<input type="text" value={form.holder} onChange={(e) => setForm({ ...form, holder: e.target.value })} required placeholder="Nombre del titular" />
</div>
<div className="form-group">
<label className="form-label">IBAN *</label>
<input type="text" value={form.iban} onChange={(e) => setForm({ ...form, iban: e.target.value })} required placeholder="ES00 0000 0000 00 0000000000" />
</div>
<div className="form-group">
<label className="form-label">Banco</label>
<input type="text" list="bank-names-list" value={form.bankName} onChange={(e) => setForm({ ...form, bankName: e.target.value })} placeholder="Ej: Banco Santander" />
</div>
<div className="form-group">
<label className="form-label">SWIFT/BIC</label>
<input type="text" value={form.swiftBic} onChange={(e) => setForm({ ...form, swiftBic: e.target.value })} placeholder="Ej: BSCHESMM" />
</div>
<div className="form-group">
<label className="form-label">Divisa</label>
<select value={form.currency} onChange={(e) => setForm({ ...form, currency: e.target.value })}>
<option value="EUR">EUR</option>
<option value="USD">USD</option>
<option value="GBP">GBP</option>
<option value="CHF">CHF</option>
</select>
</div>
<div className="form-group">
<label className="form-label">&nbsp;</label>
<label className="checkbox-label">
<input type="checkbox" checked={form.isDefault} onChange={(e) => setForm({ ...form, isDefault: e.target.checked })} />
Cuenta por defecto
</label>
</div>
</div>
<div className="form-group full-width">
<label className="form-label">Descripción</label>
<textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Notas adicionales..." style={{ minHeight: '80px' }} />
</div>
<datalist id="bank-names-list">{bankNames.map(name => <option key={name} value={name} />)}</datalist>
</form>
</div>
</>
);
}