Actualizar contabilidad.html
This commit is contained in:
@@ -443,6 +443,257 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// LÓGICA DE PESTAÑAS
|
||||||
|
// ==========================================
|
||||||
|
function toggleTab(tab) {
|
||||||
|
const vFin = document.querySelector('.max-w-6xl > div:nth-child(3)'); // Los KPIs
|
||||||
|
const vSearch = document.querySelector('.max-w-6xl > div:nth-child(4)'); // Barra busqueda
|
||||||
|
const vTable = document.querySelector('.max-w-6xl > div:nth-child(5)'); // Tabla
|
||||||
|
const vPres = document.getElementById('view-presupuestos');
|
||||||
|
|
||||||
|
const tCobros = document.getElementById('tab-cobros');
|
||||||
|
const tPres = document.getElementById('tab-presupuestos');
|
||||||
|
|
||||||
|
if (tab === 'cobros') {
|
||||||
|
vFin.classList.remove('hidden'); vSearch.classList.remove('hidden'); vTable.classList.remove('hidden');
|
||||||
|
vPres.classList.add('hidden');
|
||||||
|
tCobros.className = "px-6 py-3 font-black text-sm text-emerald-600 border-b-2 border-emerald-500 flex items-center gap-2 transition-colors";
|
||||||
|
tPres.className = "px-6 py-3 font-bold text-sm text-slate-400 border-b-2 border-transparent hover:text-slate-600 transition-colors flex items-center gap-2";
|
||||||
|
} else {
|
||||||
|
vFin.classList.add('hidden'); vSearch.classList.add('hidden'); vTable.classList.add('hidden');
|
||||||
|
vPres.classList.remove('hidden');
|
||||||
|
tPres.className = "px-6 py-3 font-black text-sm text-blue-600 border-b-2 border-blue-500 flex items-center gap-2 transition-colors";
|
||||||
|
tCobros.className = "px-6 py-3 font-bold text-sm text-slate-400 border-b-2 border-transparent hover:text-slate-600 transition-colors flex items-center gap-2";
|
||||||
|
loadBudgets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// LÓGICA DE PRESUPUESTOS Y ARTÍCULOS
|
||||||
|
// ==========================================
|
||||||
|
let myArticles = [];
|
||||||
|
let myBudgets = [];
|
||||||
|
|
||||||
|
async function loadBudgets() {
|
||||||
|
try {
|
||||||
|
// Cargamos presupuestos y artículos a la vez
|
||||||
|
const [rB, rA] = await Promise.all([
|
||||||
|
fetch(`${API_URL}/budgets`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }),
|
||||||
|
fetch(`${API_URL}/articles`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } })
|
||||||
|
]);
|
||||||
|
const dataB = await rB.json();
|
||||||
|
const dataA = await rA.json();
|
||||||
|
|
||||||
|
if (dataB.ok) myBudgets = dataB.budgets;
|
||||||
|
if (dataA.ok) myArticles = dataA.articles;
|
||||||
|
|
||||||
|
renderBudgets();
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBudgets() {
|
||||||
|
const list = document.getElementById('budgetsList');
|
||||||
|
list.innerHTML = "";
|
||||||
|
if(myBudgets.length === 0) { list.innerHTML = `<div class="p-6 text-center text-slate-400">Sin presupuestos</div>`; return; }
|
||||||
|
|
||||||
|
myBudgets.forEach(b => {
|
||||||
|
const date = new Date(b.created_at).toLocaleDateString('es-ES');
|
||||||
|
|
||||||
|
let bStatus = '';
|
||||||
|
if(b.status === 'pending') bStatus = `<span class="bg-amber-100 text-amber-700 px-2 py-1 rounded text-[10px] font-black uppercase"><i data-lucide="clock" class="w-3 h-3 inline"></i> Pte. Resolver</span>`;
|
||||||
|
if(b.status === 'rejected') bStatus = `<span class="bg-red-100 text-red-700 px-2 py-1 rounded text-[10px] font-black uppercase"><i data-lucide="x" class="w-3 h-3 inline"></i> Rechazado</span>`;
|
||||||
|
if(b.status === 'accepted') bStatus = `<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-[10px] font-black uppercase"><i data-lucide="check" class="w-3 h-3 inline"></i> Aceptado</span>`;
|
||||||
|
if(b.status === 'converted') bStatus = `<span class="bg-emerald-100 text-emerald-700 px-2 py-1 rounded text-[10px] font-black uppercase"><i data-lucide="briefcase" class="w-3 h-3 inline"></i> Es Servicio</span>`;
|
||||||
|
|
||||||
|
let actions = '';
|
||||||
|
if(b.status === 'pending') {
|
||||||
|
actions = `
|
||||||
|
<button onclick="updateBudgetStatus(${b.id}, 'accepted')" class="bg-blue-50 text-blue-600 hover:bg-blue-500 hover:text-white p-2 rounded shadow-sm transition-colors" title="Aceptar Presupuesto"><i data-lucide="thumbs-up" class="w-4 h-4"></i></button>
|
||||||
|
<button onclick="updateBudgetStatus(${b.id}, 'rejected')" class="bg-red-50 text-red-600 hover:bg-red-500 hover:text-white p-2 rounded shadow-sm transition-colors" title="Rechazar"><i data-lucide="thumbs-down" class="w-4 h-4"></i></button>
|
||||||
|
`;
|
||||||
|
} else if(b.status === 'accepted') {
|
||||||
|
actions = `<button onclick="openConvertModal(${b.id})" class="bg-emerald-500 text-white px-3 py-1.5 rounded font-black text-[10px] uppercase tracking-widest shadow-md hover:bg-emerald-600">Crear Servicio</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML += `
|
||||||
|
<div class="grid grid-cols-12 gap-4 p-4 items-center hover:bg-slate-50 transition-colors">
|
||||||
|
<div class="col-span-4 pl-2 min-w-0">
|
||||||
|
<p class="text-xs font-black text-slate-800 uppercase truncate">${b.client_name}</p>
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 mt-0.5"><i data-lucide="calendar" class="w-3 h-3 inline"></i> ${date} - 📞 ${b.client_phone}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3 min-w-0"><p class="text-[10px] text-slate-500 truncate">${b.client_address}</p></div>
|
||||||
|
<div class="col-span-2 text-center"><p class="text-sm font-black text-slate-800">${b.total}€</p></div>
|
||||||
|
<div class="col-span-3 flex justify-center items-center gap-2">${bStatus} ${actions}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBudgetStatus(id, status) {
|
||||||
|
if(!confirm("¿Actualizar estado?")) return;
|
||||||
|
await fetch(`${API_URL}/budgets/${id}/status`, { method: 'PATCH', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({status}) });
|
||||||
|
loadBudgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConvertModal(id) {
|
||||||
|
document.getElementById('convBudgetId').value = id;
|
||||||
|
document.getElementById('convertModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmConversion() {
|
||||||
|
const id = document.getElementById('convBudgetId').value;
|
||||||
|
const date = document.getElementById('convDate').value;
|
||||||
|
const time = document.getElementById('convTime').value;
|
||||||
|
if(!date || !time) return showToast("Debes elegir fecha y hora.");
|
||||||
|
|
||||||
|
await fetch(`${API_URL}/budgets/${id}/convert`, { method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({date, time}) });
|
||||||
|
document.getElementById('convertModal').classList.add('hidden');
|
||||||
|
showToast("¡Convertido a Servicio con éxito!");
|
||||||
|
loadBudgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FORMULARIO NUEVO PRESUPUESTO ---
|
||||||
|
function openBudgetModal() {
|
||||||
|
document.getElementById('bPhone').value = ""; document.getElementById('bName').value = ""; document.getElementById('bAddress').value = "";
|
||||||
|
document.getElementById('budgetLines').innerHTML = "";
|
||||||
|
addBudgetLine(); // Crea una línea vacía
|
||||||
|
calcBudget();
|
||||||
|
document.getElementById('budgetModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchClientByPhone() {
|
||||||
|
const phone = document.getElementById('bPhone').value;
|
||||||
|
if(!phone || phone.length < 9) return;
|
||||||
|
const res = await fetch(`${API_URL}/clients/search?phone=${encodeURIComponent(phone)}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.client) {
|
||||||
|
document.getElementById('bName').value = data.client.full_name;
|
||||||
|
if(data.client.addresses && data.client.addresses.length > 0) document.getElementById('bAddress').value = data.client.addresses[0];
|
||||||
|
showToast("Datos de cliente cargados");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBudgetLine() {
|
||||||
|
const lineId = Date.now();
|
||||||
|
const artOptions = myArticles.map(a => `<option value="${a.name}|${a.price}">${a.name} (${a.price}€)</option>`).join("");
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 items-center bg-white p-2 rounded-xl border border-slate-100 shadow-sm" id="bl-${lineId}">
|
||||||
|
<select onchange="applyArticleToLine(${lineId}, this.value)" class="w-full md:w-1/3 border border-slate-200 rounded px-2 py-1.5 text-xs outline-none">
|
||||||
|
<option value="">Elegir catálogo...</option>${artOptions}
|
||||||
|
</select>
|
||||||
|
<input type="text" class="b-concept w-full border border-slate-200 rounded px-2 py-1.5 text-xs font-bold outline-none" placeholder="Concepto manual...">
|
||||||
|
<div class="flex items-center gap-2 w-full md:w-auto">
|
||||||
|
<input type="number" class="b-qty w-16 border border-slate-200 rounded px-2 py-1.5 text-xs text-center outline-none" value="1" oninput="calcBudget()">
|
||||||
|
<div class="relative w-24">
|
||||||
|
<input type="number" class="b-price w-full border border-slate-200 rounded pl-2 pr-6 py-1.5 text-xs text-right outline-none" value="0.00" oninput="calcBudget()">
|
||||||
|
<span class="absolute right-2 top-1.5 text-xs text-slate-400">€</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="document.getElementById('bl-${lineId}').remove(); calcBudget();" class="text-red-400 hover:text-red-600 p-1"><i data-lucide="trash" class="w-4 h-4"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('budgetLines').insertAdjacentHTML('beforeend', html);
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyArticleToLine(lineId, val) {
|
||||||
|
if(!val) return;
|
||||||
|
const [name, price] = val.split('|');
|
||||||
|
const line = document.getElementById(`bl-${lineId}`);
|
||||||
|
line.querySelector('.b-concept').value = name;
|
||||||
|
line.querySelector('.b-price').value = price;
|
||||||
|
calcBudget();
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcBudget() {
|
||||||
|
let sub = 0;
|
||||||
|
document.querySelectorAll('#budgetLines > div').forEach(line => {
|
||||||
|
const q = parseFloat(line.querySelector('.b-qty').value) || 0;
|
||||||
|
const p = parseFloat(line.querySelector('.b-price').value) || 0;
|
||||||
|
sub += (q * p);
|
||||||
|
});
|
||||||
|
const tax = sub * 0.21;
|
||||||
|
const tot = sub + tax;
|
||||||
|
|
||||||
|
document.getElementById('bSubtotal').innerText = sub.toFixed(2) + "€";
|
||||||
|
document.getElementById('bTax').innerText = tax.toFixed(2) + "€";
|
||||||
|
document.getElementById('bTotal').innerText = tot.toFixed(2) + "€";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBudget() {
|
||||||
|
const phone = document.getElementById('bPhone').value;
|
||||||
|
const name = document.getElementById('bName').value;
|
||||||
|
if(!name) return showToast("El nombre es obligatorio");
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
document.querySelectorAll('#budgetLines > div').forEach(line => {
|
||||||
|
const c = line.querySelector('.b-concept').value;
|
||||||
|
const q = parseFloat(line.querySelector('.b-qty').value) || 0;
|
||||||
|
const p = parseFloat(line.querySelector('.b-price').value) || 0;
|
||||||
|
if(c && q > 0) items.push({concept: c, qty: q, price: p});
|
||||||
|
});
|
||||||
|
|
||||||
|
if(items.length === 0) return showToast("Añade al menos un concepto");
|
||||||
|
|
||||||
|
const sub = parseFloat(document.getElementById('bSubtotal').innerText);
|
||||||
|
const tax = parseFloat(document.getElementById('bTax').innerText);
|
||||||
|
const tot = parseFloat(document.getElementById('bTotal').innerText);
|
||||||
|
|
||||||
|
const payload = { client_phone: phone, client_name: name, client_address: document.getElementById('bAddress').value, items, subtotal: sub, tax, total: tot };
|
||||||
|
|
||||||
|
await fetch(`${API_URL}/budgets`, { method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify(payload) });
|
||||||
|
|
||||||
|
document.getElementById('budgetModal').classList.add('hidden');
|
||||||
|
showToast("Presupuesto guardado");
|
||||||
|
loadBudgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CATÁLOGO DE ARTÍCULOS ---
|
||||||
|
function openArticlesModal() {
|
||||||
|
renderArticles();
|
||||||
|
document.getElementById('articlesModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderArticles() {
|
||||||
|
const list = document.getElementById('articlesList');
|
||||||
|
list.innerHTML = myArticles.map(a => `
|
||||||
|
<div class="flex justify-between items-center bg-white p-3 rounded-xl border border-slate-100 shadow-sm">
|
||||||
|
<span class="font-bold text-xs uppercase text-slate-700">${a.name}</span>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="font-black text-emerald-600">${a.price}€</span>
|
||||||
|
<button onclick="editArticle(${a.id}, '${a.name}', ${a.price})" class="text-blue-500 hover:text-blue-700"><i data-lucide="edit" class="w-4 h-4"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveArticle() {
|
||||||
|
const name = document.getElementById('newArtName').value;
|
||||||
|
const price = document.getElementById('newArtPrice').value;
|
||||||
|
if(!name || !price) return;
|
||||||
|
await fetch(`${API_URL}/articles`, { method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({name, price}) });
|
||||||
|
document.getElementById('newArtName').value = ""; document.getElementById('newArtPrice').value = "";
|
||||||
|
await loadBudgets(); // Recarga y pinta
|
||||||
|
renderArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editArticle(id, oldName, oldPrice) {
|
||||||
|
const n = prompt("Nuevo nombre:", oldName);
|
||||||
|
const p = prompt("Nuevo precio:", oldPrice);
|
||||||
|
if(n && p) {
|
||||||
|
await fetch(`${API_URL}/articles/${id}`, { method: 'PUT', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({name: n, price: p}) });
|
||||||
|
await loadBudgets(); renderArticles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function showToast(msg) {
|
function showToast(msg) {
|
||||||
const t = document.getElementById('toast');
|
const t = document.getElementById('toast');
|
||||||
t.innerHTML = `<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400"></i> ${msg}`;
|
t.innerHTML = `<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400"></i> ${msg}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user