Help
RSS
API
Feed
Maltego
Contact
Domain > controldeprestamos.com
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2020-02-19
104.27.151.141
(
ClassC
)
2026-01-08
77.37.76.245
(
ClassC
)
Port 80
HTTP/1.1 301 Moved PermanentlyDate: Thu, 08 Jan 2026 06:26:35 GMTContent-Type: text/htmlContent-Length: 795Connection: keep-aliveLocation: https://controldeprestamos.com/platform: hostingerpanel: hpanelContent-Security-Policy: upgrade-insecure-requestsServer: hcdnalt-svc: h3:443; ma86400x-hcdn-request-id: 00bbfe84eec7de183c790fd99033c251-phx-edge6x-hcdn-cache-status: MISSx-hcdn-upstream-rt: 0.128 !DOCTYPE html>html styleheight:100%>head>meta nameviewport contentwidthdevice-width, initial-scale1, shrink-to-fitno />title> 301 Moved Permanently/title>style>@media (prefers-color-scheme:dark){body{background-color:#000!important}}/style>/head>body stylecolor: #444; margin:0;font: normal 14px/20px Arial, Helvetica, sans-serif; height:100%; background-color: #fff;>div styleheight:auto; min-height:100%; > div styletext-align: center; width:800px; margin-left: -400px; position:absolute; top: 30%; left:50%;> h1 stylemargin:0; font-size:150px; line-height:150px; font-weight:bold;>301/h1>h2 stylemargin-top:20px;font-size: 30px;>Moved Permanently/h2>p>The document has been permanently moved./p>/div>/div>/body>/html>
Port 443
HTTP/1.1 200 OKDate: Thu, 08 Jan 2026 06:26:36 GMTContent-Type: text/htmlTransfer-Encoding: chunkedConnection: keep-aliveVary: Accept-EncodingLast-Modified: Fri, 28 Nov 2025 02:18:53 GMTEtag: W/9278-6929068d-2bd1efbff8ee2ab1;gzplatform: hostingerpanel: hpanelContent-Security-Policy: upgrade-insecure-requestsServer: hcdnalt-svc: h3:443; ma86400x-hcdn-request-id: 5ebf7c0532326f1fc15b79fdb6499f04-phx-edge6x-hcdn-cache-status: DYNAMICx-hcdn-upstream-rt: 0.199 !DOCTYPE html>html langes>head> meta charsetUTF-8 /> title>Control de Préstamos entre Personas | App Web Gratis/title> meta nameviewport contentwidthdevice-width, initial-scale1 /> meta nametheme-color content#2563eb> !-- SEO --> meta namedescription contentControl de Préstamos es una app web gratuita para llevar el control de préstamos y deudas entre familiares y amigos. Registra quién debe a quién, abonos, pagos y saldos en tiempo real. Funciona offline como PWA y no requiere registro.> meta namekeywords contentcontrol de préstamos, control de deudas, app de préstamos entre amigos, registrar deudas, mini préstamos, control de dinero prestado, app web gratis, PWA préstamos> meta nameauthor contentControl de Préstamos> !-- Open Graph --> meta propertyog:title contentControl de Préstamos entre Personas | App Web Gratis> meta propertyog:description contentRegistra fácilmente mini préstamos, abonos y pagos entre hermanos, familia, pareja y amigos. Ve el saldo al instante y úsala offline como app instalada.> meta propertyog:type contentwebsite> meta propertyog:url content> meta propertyog:image contenticons/icon-512.png> link relmanifest hrefmanifest.webmanifest> link relapple-touch-icon hreficons/icon-192.png> meta nameapple-mobile-web-app-capable contentyes> meta nameapple-mobile-web-app-status-bar-style contentblack-translucent> style> :root { --bg: #f6f6f9; --card: #ffffff; --accent: #2563eb; --danger: #dc2626; --success: #16a34a; --text: #111827; --muted: #6b7280; --border: #e5e7eb; } bodydata-themedark { --bg: #020617; --card: #020617; --accent: #3b82f6; --danger: #f87171; --success: #4ade80; --text: #e5e7eb; --muted: #9ca3af; --border: #1f2937; } * { box-sizing:border-box; } body { margin:0; font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; background:var(--bg); color:var(--text); padding:1rem; } .app { max-width:800px; margin:0 auto; display:flex; flex-direction:column; gap:1rem; } .card { background:var(--card); border-radius:.9rem; padding:1.1rem 1.2rem; border:1px solid var(--border); box-shadow:0 8px 24px rgba(15,23,42,.08); } .header { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:.5rem; } .title { font-size:1.3rem;font-weight:700; } .subtitle { font-size:.85rem;color:var(--muted); } .row { display:flex;flex-wrap:wrap;gap:.5rem;align-items:center; } .row.space-between { justify-content:space-between; } .btn { border:none; border-radius:999px; padding:.35rem .8rem; font-size:.8rem; cursor:pointer; display:inline-flex; align-items:center; gap:.25rem; } .btn-primary { background:var(--accent);color:#fff; } .btn-outline { background:transparent;border:1px solid var(--border);color:var(--text); } .btn-danger { background:var(--danger);color:#fff; } .btn-sm { padding:.2rem .6rem;font-size:.75rem; } select,input,textarea { font-family:inherit;font-size:.85rem; } .field { display:flex;flex-direction:column;gap:.25rem;margin-bottom:.6rem;font-size:.8rem; } .field input,.field select,.field textarea { padding:.3rem .45rem; border-radius:.5rem; border:1px solid var(--border); background:var(--bg); color:var(--text); } .persons-list { display:flex;flex-direction:column;gap:.5rem;margin-top:.75rem; } .person-item { display:grid; grid-template-columns: minmax(0,2fr) minmax(0,1fr) auto; gap:.5rem; align-items:center; padding:.5rem .6rem; border-radius:.6rem; border:1px solid var(--border); background:var(--bg); font-size:.85rem; } @media(max-width:600px){ .person-item{grid-template-columns:minmax(0,2fr) minmax(0,1fr);} } .person-name { font-weight:600; } .person-note { color:var(--muted);font-size:.75rem; } .person-balance { text-align:right;font-weight:700;font-size:.85rem; } .positive { color:var(--success); } .negative { color:var(--danger); } .zero { color:var(--muted); } .hidden { display:none !important; } .balance-box { text-align:center;margin-top:.5rem; } .balance-label { font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;color:var(--muted); } .balance-amount { font-size:2rem;font-weight:800;margin:.2rem 0; } .balance-text { font-size:.85rem;color:var(--muted); } .summary { display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.75rem;font-size:.8rem; } .summary-item { flex:1;min-width:120px;padding:.5rem;border-radius:.6rem;background:var(--bg);border:1px solid var(--border); } .summary-label { color:var(--muted);font-size:.75rem;margin-bottom:.15rem; } .summary-value { font-weight:600; } .movements { margin-top:.5rem;display:flex;flex-direction:column;gap:.4rem;max-height:320px;overflow:auto; } .movement { display:grid; grid-template-columns:minmax(0,2fr) minmax(0,1fr) auto; gap:.35rem .6rem; align-items:center; padding:.45rem .5rem; border-radius:.6rem; border:1px solid var(--border); background:var(--bg); font-size:.8rem; } @media(max-width:600px){ .movement{grid-template-columns:minmax(0,2fr) minmax(0,1fr);} } .movement-main{display:flex;flex-direction:column;gap:.1rem;} .movement-type{font-weight:600;} .movement-note{color:var(--muted);} .movement-meta{font-size:.7rem;color:var(--muted);display:flex;flex-direction:column;} .movement-amount{text-align:right;font-weight:700;font-size:.85rem;} .movement-actions{display:flex;flex-wrap:wrap;gap:.25rem;justify-content:flex-end;} .filters{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.4rem;font-size:.75rem;align-items:center;} .filters input,.filters select{font-size:.75rem;padding:.2rem .4rem;border-radius:.5rem;border:1px solid var(--border);background:var(--bg);color:var(--text);} .empty{font-size:.8rem;color:var(--muted);text-align:center;padding:.6rem 0;} /* Landing / SEO secciones */ .landing-title { font-size:1.6rem; font-weight:800; margin-bottom:.4rem; } .landing-highlight { display:inline-flex; align-items:center; gap:.3rem; font-size:.8rem; padding:.18rem .6rem; border-radius:999px; background:rgba(37,99,235,.08); color:var(--accent); margin-bottom:.4rem; } .landing-grid { display:grid; grid-template-columns: minmax(0,1.6fr) minmax(0,1.4fr); gap:1rem; margin-top:.7rem; font-size:.85rem; } @media(max-width:768px){ .landing-grid{grid-template-columns:1fr;} } .landing-list { list-style:none; padding:0; margin:.4rem 0; } .landing-list li { display:flex; align-items:flex-start; gap:.4rem; margin-bottom:.3rem; } .landing-list li::before { content:✔; color:var(--success); font-size:.8rem; margin-top:.1rem; } .pill { display:inline-flex; align-items:center; padding:.18rem .55rem; border-radius:999px; background:var(--bg); border:1px solid var(--border); font-size:.75rem; color:var(--muted); margin-right:.2rem; margin-bottom:.2rem; } /* ESTADOS: landing colapsada */ #landingSection.collapsed { padding:.6rem .8rem; margin-bottom:.4rem; } #landingSection.collapsed .landing-highlight, #landingSection.collapsed .landing-main-text, #landingSection.collapsed .landing-grid { display:none; } #landingSection.collapsed .landing-title { font-size:1.1rem; margin-bottom:0; } /style>/head>body data-themelight> div classapp> !-- LANDING / SEO --> section classcard aria-labelledbylanding-title idlandingSection> div classlanding-highlight> 💸 Control simple de mini préstamos entre personas /div> h1 idlanding-title classlanding-title> Control de Préstamos entre familiares y amigos, en una sola app web /h1> p classlanding-main-text stylefont-size:.9rem;max-width:42rem;margin:0 0 .6rem;> Control de Préstamos es una pequeña app web pensada para el día a día: anota cuánto te presta tu hermano, mamá, pareja o amigos, registra abonos y pagos y mira el saldo final al instante, sin hojas de Excel ni libretas. /p> div classrow stylemargin:.3rem 0 .7rem;justify-content:space-between;align-items:center;> div classrow> button iduseAppBtn classbtn btn-primary> Usar la app ahora /button> span stylefont-size:.8rem;color:var(--muted);> Sin registro · Funciona offline como PWA · Todo en tu dispositivo /span> /div> button idtoggleLandingBtn classbtn btn-outline btn-sm typebutton> Reducir explicación /button> /div> div classlanding-grid> div> h2 stylefont-size:.95rem;margin:0 0 .2rem;>¿Qué puedes hacer con esta app?/h2> ul classlanding-list> li>Registrar mini préstamos entre tú y varias personas (hermano, mamá, amigos, roommates, etc.)./li> li>Ver de un vistazo quién te debe y a quién le debes tú, con saldos claros y en color./li> li>Guardar abonos y pagos sin complicarte: la app decide sola si es préstamo o pago según el saldo actual./li> li>Filtrar por persona, tipo de movimiento, categoría o nota para encontrar cualquier registro./li> /ul> h3 stylefont-size:.9rem;margin:.5rem 0 .2rem;>Ideal para/h3> div> span classpill>Préstamos entre hermanos/span> span classpill>Deudas con mamá o papá/span> span classpill>Amigos y roommates/span> span classpill>Pequeños favores de dinero/span> /div> /div> div> h2 stylefont-size:.95rem;margin:0 0 .25rem;>Ventajas frente a una hoja de cálculo/h2> ul classlanding-list> li>Interfaz pensada solo para préstamos entre tú y cada persona./li> li>Funciona como app instalada en tu móvil (PWA), incluso sin conexión./li> li>No envía tus datos a ningún servidor: todo se guarda en el navegador./li> li>Exporta e importa tus movimientos en formato JSON para hacer copias de seguridad./li> /ul> p stylefont-size:.8rem;color:var(--muted);margin-top:.4rem;> Sigue bajando para ver la app en acción y empezar a registrar tus préstamos ahora mismo. /p> /div> /div> /section> !-- APP REAL (FUNCIONAL) --> section idappSection> header classheader card stylemargin-top:.4rem;> div> div classtitle>Control de Préstamos/div> div classsubtitle>Mini préstamos entre tú y varias personas 💸/div> /div> div classrow> div classrow> span stylefont-size:.8rem;>Moneda:/span> select idcurrencySelect> option valueMXN>$ MXN/option> option valueEUR>€ EUR/option> option valueUSD>$ USD/option> /select> /div> div classrow> span stylefont-size:.8rem;>Modo oscuro/span> label classswitch styleposition:relative;width:40px;height:22px;> input typecheckbox iddarkModeToggle styleopacity:0;width:0;height:0;> span classslider styleposition:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#9ca3af;border-radius:999px;transition:.3s;>/span> /label> /div> /div> /header> section idpersonsView classcard> div classrow space-between> div> strong>Personas/strong> div stylefont-size:.8rem;color:var(--muted);> Total: span idpersonsCount>0/span> /div> /div> div stylefont-size:.8rem;color:var(--muted);> Movimientos: span idglobalMovementsCount>0/span> /div> /div> div classrow stylemargin-top:.6rem;> input idnewPersonName typetext placeholderNombre (Hermano, Mamá, Ana...) styleflex:1;min-width:160px;padding:.3rem .45rem;border-radius:.5rem;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:.85rem;> button idaddPersonBtn classbtn btn-primary btn-sm>Agregar/button> /div> div idpersonsList classpersons-list>/div> /section> section idpersonDetailView classhidden> section classcard> div classrow stylemargin-bottom:.4rem;> button idbackBtn classbtn btn-outline btn-sm>← Volver/button> /div> div classrow space-between> div> div idpersonTitle classtitle stylefont-size:1.1rem;>/div> div idpersonSubtitle classsubtitle>/div> /div> div stylefont-size:.8rem;color:var(--muted);> Movimientos: span idpersonMovementsCount>0/span> /div> /div> div classbalance-box> div classbalance-label>Saldo neto con esta persona/div> div idbalanceAmount classbalance-amount zero>$0.00/div> div idbalanceText classbalance-text>No hay deudas pendientes./div> div stylemargin-top:.5rem;> button idtoggleFormBtn classbtn btn-primary btn-sm>Agregar movimiento/button> /div> /div> div classsummary> div classsummary-item> div classsummary-label>Total que tú entregaste/div> div idsumFromMe classsummary-value>$0.00/div> /div> div classsummary-item> div classsummary-label>Total que él/ella entregó/div> div idsumFromOther classsummary-value>$0.00/div> /div> /div> /section> section classcard idformCard stylemargin-top:.5rem;display:none;> h3 stylemargin-top:0;font-size:.95rem;>Nuevo movimiento/h3> p stylefont-size:.8rem;color:var(--muted);margin-top:0;> Registra quién entregó dinero y cuánto entre tú y span idotherNameInline>esta persona/span>. /p> form idmovementForm> div classfield> label foractor>Quién entrega el dinero/label> select idactor required> option valueme>Yo le entrego dinero/option> option valueother>Él/Ella me entrega dinero/option> /select> small stylecolor:var(--muted);font-size:.75rem;> La app decide sola si es préstamo o abono según el saldo actual. /small> /div> div classrow> div classfield styleflex:1;> label foramount>Monto/label> input idamount typenumber min0.01 step0.01 required> /div> div classfield styleflex:1;> label fordate>Fecha/label> input iddate typedate required> /div> /div> div classfield> label forcategory>Categoría (opcional)/label> input idcategory typetext placeholderComida, Uber, Cine...> /div> div classfield> label fornote>Nota (opcional)/label> textarea idnote rows2 placeholderDetalle breve...>/textarea> /div> div classrow stylejustify-content:flex-end;margin-top:.4rem;> button typebutton idresetFormBtn classbtn btn-outline btn-sm>Limpiar/button> button typesubmit classbtn btn-primary btn-sm>Guardar/button> /div> /form> /section> section classcard stylemargin-top:.5rem;> div classrow space-between> strong>Historial/strong> div stylefont-size:.75rem;color:var(--muted);>Los más recientes arriba/div> /div> div classfilters> span>Filtros:/span> select idfilterActor> option valueall>Todos/option> option valueme>Yo entregué/option> option valueother>Él/Ella entregó/option> /select> select idfilterKind> option valueall>Todos los tipos/option> option valueloan>Préstamos/option> option valuepayment>Pagos / abonos/option> /select> input idfilterCategory typetext placeholderCategoría> input idfilterSearch typetext placeholderBuscar nota> /div> div classrow stylejustify-content:flex-end;margin-top:.4rem;> button idexportBtn classbtn btn-outline btn-sm>Exportar JSON/button> label classbtn btn-outline btn-sm stylecursor:pointer;> Importar JSON input idimportFile typefile acceptapplication/json styledisplay:none;> /label> /div> div idmovements classmovements>/div> /section> /section> /section>!-- /appSection --> /div> script> const STORAGE_KEY miniLoansMultiPersons_v2; const defaultState { currency: MXN, darkMode: false, persons: { id: p1, name: Hermano }, movements: }; let state loadState(); let currentPersonId null; let landingCollapsed false; function loadState(){ try{ const raw localStorage.getItem(STORAGE_KEY); if(!raw) return { ...defaultState }; const parsed JSON.parse(raw); parsed.persons parsed.persons && parsed.persons.length ? parsed.persons : ...defaultState.persons; parsed.movements (parsed.movements || ).map(m > migrateMovement(m)); return { ...defaultState, ...parsed }; }catch(e){ console.error(e); return { ...defaultState }; } } function migrateMovement(m){ const c { ...m }; if(!c.personId) c.personId p1; if(!c.actor && c.direction) c.actor c.direction favor_me ? me : other; if(!c.actor) c.actor me; if(!c.kind) c.kind loan; return c; } function saveState(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function formatMoney(v){ const cur state.currency || MXN; const sym cur EUR ? € : $; return sym + Number(v || 0).toFixed(2); } function computeBalanceForPerson(pid){ let bal 0; for(const m of state.movements){ if(m.personId ! pid) continue; const a Number(m.amount); if(!a || a 0) continue; bal + m.actor me ? a : -a; } return bal; } function summarizeForPerson(pid){ let fromMe 0, fromOther 0; for(const m of state.movements){ if(m.personId ! pid) continue; const a Number(m.amount); if(!a || a 0) continue; if(m.actor me) fromMe + a; else fromOther + a; } return { fromMe, fromOther }; } function humanTypeLabel(m, otherName){ const other otherName || la otra persona; if(m.actor me && m.kind loan) return Tú le prestaste a + other; if(m.actor other && m.kind loan) return other + te prestó; if(m.actor me && m.kind payment) return Tú le pagaste a + other; if(m.actor other && m.kind payment) return other + te pagó; return Movimiento; } function previewDelta(m){ const a Number(m.amount); if(!a || a 0) return 0; return m.actor me ? a : -a; } const currencySelect document.getElementById(currencySelect); const darkModeToggle document.getElementById(darkModeToggle); const personsView document.getElementById(personsView); const personDetailView document.getElementById(personDetailView); const personsCountEl document.getElementById(personsCount); const globalMovementsCountEl document.getElementById(globalMovementsCount); const personsListEl document.getElementById(personsList); const newPersonNameInput document.getElementById(newPersonName); const addPersonBtn document.getElementById(addPersonBtn); const backBtn document.getElementById(backBtn); const personTitleEl document.getElementById(personTitle); const personSubtitleEl document.getElementById(personSubtitle); const personMovementsCountEl document.getElementById(personMovementsCount); const balanceAmountEl document.getElementById(balanceAmount); const balanceTextEl document.getElementById(balanceText); const sumFromMeEl document.getElementById(sumFromMe); const sumFromOtherEl document.getElementById(sumFromOther); const otherNameInline document.getElementById(otherNameInline); const toggleFormBtn document.getElementById(toggleFormBtn); const formCard document.getElementById(formCard); const movementForm document.getElementById(movementForm); const actorSelect document.getElementById(actor); const amountInput document.getElementById(amount); const dateInput document.getElementById(date); const categoryInput document.getElementById(category); const noteInput document.getElementById(note); const resetFormBtn document.getElementById(resetFormBtn); const filterActor document.getElementById(filterActor); const filterKind document.getElementById(filterKind); const filterCategory document.getElementById(filterCategory); const filterSearch document.getElementById(filterSearch); const movementsContainer document.getElementById(movements); const exportBtn document.getElementById(exportBtn); const importFile document.getElementById(importFile); const landingSection document.getElementById(landingSection); const toggleLandingBtn document.getElementById(toggleLandingBtn); const useAppBtn document.getElementById(useAppBtn); const appSection document.getElementById(appSection); function setLandingCollapsed(collapsed){ landingCollapsed collapsed; if(collapsed){ landingSection.classList.add(collapsed); toggleLandingBtn.textContent Mostrar explicación; } else { landingSection.classList.remove(collapsed); toggleLandingBtn.textContent Reducir explicación; } } function initUI(){ currencySelect.value state.currency; darkModeToggle.checked !!state.darkMode; document.body.dataset.theme state.darkMode ? dark : light; if(!dateInput.value){ dateInput.value new Date().toISOString().split(T)0; } setLandingCollapsed(false); // por defecto expandida renderPersonsList(); showPersonsView(); } function showPersonsView(){ currentPersonId null; personsView.classList.remove(hidden); personDetailView.classList.add(hidden); } function showPersonDetail(pid){ currentPersonId pid; personsView.classList.add(hidden); personDetailView.classList.remove(hidden); renderPersonDetail(); } function renderPersonsList(){ personsListEl.innerHTML ; personsCountEl.textContent state.persons.length; globalMovementsCountEl.textContent state.movements.length; if(!state.persons.length){ const div document.createElement(div); div.className empty; div.textContent Aún no hay personas. Agrega a tu hermano, mamá, amigos...; personsListEl.appendChild(div); return; } for(const p of state.persons){ const row document.createElement(div); row.className person-item; const main document.createElement(div); const nameEl document.createElement(div); nameEl.className person-name; nameEl.textContent p.name; const noteEl document.createElement(div); noteEl.className person-note; const count state.movements.filter(m>m.personIdp.id).length; noteEl.textContent count ? count + movimientos : Sin movimientos; main.appendChild(nameEl); main.appendChild(noteEl); const balEl document.createElement(div); balEl.className person-balance; const bal computeBalanceForPerson(p.id); const abs Math.abs(bal); if(bal>0){ balEl.classList.add(positive); balEl.textContent formatMoney(abs) + (te debe); } else if(bal0){ balEl.classList.add(negative); balEl.textContent formatMoney(abs) + (tú debes); } else { balEl.classList.add(zero); balEl.textContent Saldo 0; } const actions document.createElement(div); actions.style.displayflex; actions.style.gap.25rem; const viewBtn document.createElement(button); viewBtn.className btn btn-primary btn-sm; viewBtn.textContent Ver; viewBtn.onclick () > showPersonDetail(p.id); const delBtn document.createElement(button); delBtn.className btn btn-danger btn-sm; delBtn.textContent Borrar; delBtn.onclick () > onDeletePerson(p.id); actions.appendChild(viewBtn); actions.appendChild(delBtn); row.appendChild(main); row.appendChild(balEl); row.appendChild(actions); personsListEl.appendChild(row); } } function renderPersonDetail(){ const person state.persons.find(p>p.idcurrentPersonId); if(!person){ showPersonsView(); return; } personTitleEl.textContent person.name; personSubtitleEl.textContent Préstamos y pagos entre tú y + person.name + .; otherNameInline.textContent person.name; const bal computeBalanceForPerson(person.id); const abs Math.abs(bal); balanceAmountEl.textContent formatMoney(abs); balanceAmountEl.classList.remove(positive,negative,zero); if(bal>0){ balanceAmountEl.classList.add(positive); balanceTextEl.textContent person.name + te debe + formatMoney(abs) + .; }else if(bal0){ balanceAmountEl.classList.add(negative); balanceTextEl.textContent Tú le debes + formatMoney(abs) + a + person.name + .; }else{ balanceAmountEl.classList.add(zero); balanceTextEl.textContent No hay deudas pendientes.; } const {fromMe,fromOther} summarizeForPerson(person.id); sumFromMeEl.textContent formatMoney(fromMe); sumFromOtherEl.textContent formatMoney(fromOther); const count state.movements.filter(m>m.personIdperson.id).length; personMovementsCountEl.textContent count; renderMovementsForPerson(); } function getFilteredMovementsForPerson(){ if(!currentPersonId) return ; const actorF filterActor.value; const kindF filterKind.value; const catF filterCategory.value.trim().toLowerCase(); const searchF filterSearch.value.trim().toLowerCase(); let list state.movements.filter(m>m.personIdcurrentPersonId); list list.sort((a,b)>{ if(a.dateb.date) return (b.createdAt||0)-(a.createdAt||0); return (b.date||).localeCompare(a.date||); }); if(actorF!all) list list.filter(m>m.actoractorF); if(kindF!all) list list.filter(m>m.kindkindF); if(catF) list list.filter(m>(m.category||).toLowerCase().includes(catF)); if(searchF) list list.filter(m>(m.note||).toLowerCase().includes(searchF)); return list; } function renderMovementsForPerson(){ movementsContainer.innerHTML; const person state.persons.find(p>p.idcurrentPersonId); const otherName person ? person.name : la otra persona; const list getFilteredMovementsForPerson(); if(!list.length){ const div document.createElement(div); div.className empty; div.textContent No hay movimientos con estos filtros.; movementsContainer.appendChild(div); return; } for(const m of list){ const row document.createElement(div); row.className movement; const main document.createElement(div); main.className movement-main; const typeEl document.createElement(div); typeEl.className movement-type; typeEl.textContent humanTypeLabel(m,otherName); const noteEl document.createElement(div); noteEl.className movement-note; noteEl.textContent m.note || Sin nota; main.appendChild(typeEl); main.appendChild(noteEl); const meta document.createElement(div); meta.className movement-meta; meta.innerHTML span>+(m.date||)+/span>span>ID: +m.id+/span>; const amountEl document.createElement(div); amountEl.className movement-amount; const d previewDelta(m); if(d>0) amountEl.classList.add(positive); else amountEl.classList.add(negative); amountEl.textContent formatMoney(m.amount); const actions document.createElement(div); actions.className movement-actions; const delBtn document.createElement(button); delBtn.className btn btn-danger btn-sm; delBtn.textContent Borrar; delBtn.onclick () > onDeleteMovement(m.id); actions.appendChild(delBtn); row.appendChild(main); row.appendChild(meta); row.appendChild(amountEl); row.appendChild(actions); movementsContainer.appendChild(row); } } currencySelect.addEventListener(change,()>{ state.currency currencySelect.value; saveState(); if(currentPersonId) renderPersonDetail(); else renderPersonsList(); }); darkModeToggle.addEventListener(change,()>{ state.darkMode darkModeToggle.checked; document.body.dataset.theme state.darkMode ? dark : light; saveState(); }); addPersonBtn.addEventListener(click,()>{ const name newPersonNameInput.value.trim(); if(!name){ alert(Escribe un nombre.); return; } const id p+Date.now().toString(36); state.persons.push({id,name}); saveState(); newPersonNameInput.value; renderPersonsList(); }); backBtn.addEventListener(click,()>showPersonsView()); function onDeletePerson(pid){ const p state.persons.find(x>x.idpid); if(!p) return; const count state.movements.filter(m>m.personIdpid).length; if(!confirm(`¿Borrar a ${p.name} y sus ${count} movimientos?`)) return; state.persons state.persons.filter(x>x.id!pid); state.movements state.movements.filter(m>m.personId!pid); saveState(); showPersonsView(); renderPersonsList(); } toggleFormBtn.addEventListener(click,()>{ const visible formCard.style.displayblock; formCard.style.display visible ? none : block; }); movementForm.addEventListener(submit,(e)>{ e.preventDefault(); if(!currentPersonId){ alert(Selecciona una persona.); return; } const actor actorSelect.value; const amount Number(amountInput.value); const date dateInput.value; const category categoryInput.value.trim(); const note noteInput.value.trim(); if(!actor || !amount || amount0 || !date){ alert(Completa quién entrega, monto y fecha.); return; } const prevBal computeBalanceForPerson(currentPersonId); let kind; if(actorme){ kind prevBal0 ? payment : loan; } else { kind prevBal>0 ? payment : loan; } const movement { id: Date.now().toString(36), personId: currentPersonId, actor, kind, amount, date, category, note, createdAt: Date.now() }; state.movements.push(movement); saveState(); movementForm.reset(); actorSelect.valueme; dateInput.value new Date().toISOString().split(T)0; renderPersonDetail(); }); resetFormBtn.addEventListener(click,()>{ movementForm.reset(); actorSelect.valueme; dateInput.value new Date().toISOString().split(T)0; }); filterActor.addEventListener(change,renderMovementsForPerson); filterKind.addEventListener(change,renderMovementsForPerson); filterCategory.addEventListener(input,renderMovementsForPerson); filterSearch.addEventListener(input,renderMovementsForPerson); function onDeleteMovement(id){ const m state.movements.find(x>x.idid); if(!m) return; if(!confirm(¿Borrar este movimiento?)) return; state.movements state.movements.filter(x>x.id!id); saveState(); renderPersonDetail(); } exportBtn.addEventListener(click,()>{ const data JSON.stringify(state,null,2); const blob new Blob(data,{type:application/json}); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download control-prestamos.json; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); importFile.addEventListener(change,(e)>{ const file e.target.files0; if(!file) return; const reader new FileReader(); reader.onload ev > { try{ const imported JSON.parse(ev.target.result); if(!imported || typeof imported!object){ alert(Archivo inválido.); return; } if(!confirm(Se reemplazarán tus datos actuales. ¿Continuar?)) return; imported.movements (imported.movements||).map(m>migrateMovement(m)); imported.persons imported.persons && imported.persons.length ? imported.persons : defaultState.persons; state { ...defaultState, ...imported }; saveState(); initUI(); alert(Datos importados.); }catch(err){ console.error(err); alert(No se pudo importar.); } }; reader.readAsText(file); e.target.value; }); // Landing: colapsar / expandir toggleLandingBtn.addEventListener(click, () > { setLandingCollapsed(!landingCollapsed); }); useAppBtn.addEventListener(click, () > { setLandingCollapsed(true); appSection.scrollIntoView({ behavior: smooth }); }); document.addEventListener(DOMContentLoaded,()>{ initUI(); }); if(serviceWorker in navigator){ window.addEventListener(load,()>{ navigator.serviceWorker.register(./sw.js).catch(err>console.error(SW error,err)); }); } /script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]