Spaces:
Running
Running
| <!-- CoreBanking AS400 v2 - Vue 3 Reactive --> | |
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>CoreBanking AS400</title> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"/> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{--bg:#07071a;--card:#111128;--border:#1e1e50;--accent:#7c3aed;--text:#e2e8f0;--muted:#64748b;--green:#10b981;--red:#ef4444;--yellow:#f59e0b} | |
| body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh} | |
| header{background:linear-gradient(135deg,#1a0a3e,#0a1a3e);padding:18px 28px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--border)} | |
| header h1{font-family:'JetBrains Mono',monospace;font-size:1.35em;color:#c4b5fd} | |
| .sub{color:var(--muted);font-size:.75em;margin-top:3px} | |
| .clock{margin-left:auto;font-family:'JetBrains Mono',monospace;color:var(--muted);font-size:.85em} | |
| .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:16px 28px} | |
| .kpi{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;text-align:center} | |
| .kpi .v{font-family:'JetBrains Mono',monospace;font-size:1.4em;font-weight:700;color:#a78bfa} | |
| .kpi .l{font-size:.72em;color:var(--muted);margin-top:3px} | |
| nav{display:flex;gap:2px;padding:0 28px;border-bottom:1px solid var(--border)} | |
| .ntab{padding:10px 16px;cursor:pointer;color:var(--muted);font-size:.85em;border-bottom:2px solid transparent;transition:.15s;white-space:nowrap} | |
| .ntab.on{color:#a78bfa;border-color:var(--accent);font-weight:600} | |
| .ntab:hover{color:var(--text)} | |
| .pane{padding:20px 28px} | |
| .grid2{display:grid;grid-template-columns:300px 1fr;gap:16px} | |
| .grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px} | |
| .card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:18px} | |
| .card h3{font-size:.8em;font-weight:700;color:#a78bfa;text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px} | |
| label{display:block;font-size:.76em;color:var(--muted);margin:8px 0 3px} | |
| input,select{width:100%;background:#0a0a1f;border:1px solid var(--border);border-radius:7px;padding:8px 11px;color:var(--text);font-size:.85em;outline:none;transition:.15s} | |
| input:focus,select:focus{border-color:var(--accent)} | |
| .btn{width:100%;padding:9px;border:none;border-radius:7px;background:linear-gradient(135deg,var(--accent),#4f46e5);color:#fff;font-weight:600;cursor:pointer;margin-top:12px;font-size:.85em;transition:.2s} | |
| .btn:hover{opacity:.85} | |
| .alert{margin-top:8px;padding:8px 11px;border-radius:7px;font-size:.8em} | |
| .ok{background:#10b98115;border:1px solid var(--green);color:var(--green)} | |
| .err{background:#ef444415;border:1px solid var(--red);color:var(--red)} | |
| table{width:100%;border-collapse:collapse;font-size:.82em} | |
| th{text-align:left;padding:9px 11px;color:var(--muted);border-bottom:1px solid var(--border);font-weight:600} | |
| td{padding:8px 11px;border-bottom:1px solid #0d0d28} | |
| tr:hover td{background:#ffffff04} | |
| .badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:.72em;font-weight:600} | |
| .g{background:#10b98120;color:var(--green)}.r{background:#ef444420;color:var(--red)}.y{background:#f59e0b20;color:var(--yellow)}.b{background:#3b82f620;color:#60a5fa} | |
| .ec{font-family:'JetBrains Mono',monospace;font-size:.78em;background:#0a0a1f;border:1px solid var(--border);border-radius:8px;padding:14px;white-space:pre;color:#a78bfa;line-height:1.8;margin-top:12px} | |
| .fade-enter-active,.fade-leave-active{transition:opacity .15s} | |
| .fade-enter-from,.fade-leave-to{opacity:0} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <header> | |
| <span style="font-size:1.5em">⚙️</span> | |
| <div><h1>CoreBanking AS400 · Sistema Demo</h1><div class="sub">ILE RPG · DB2 for i · CLP · SIGNATURE / ICBS</div></div> | |
| <div class="clock">{{clock}}</div> | |
| </header> | |
| <div class="kpis"> | |
| <div class="kpi"><div class="v">{{totalClientes}}</div><div class="l">👥 Clientes</div></div> | |
| <div class="kpi"><div class="v">{{totalCuentas}}</div><div class="l">🏦 Cuentas</div></div> | |
| <div class="kpi"><div class="v">${{(totalSaldo/1e6).toFixed(1)}}M</div><div class="l">💰 Saldo MXN</div></div> | |
| <div class="kpi"><div class="v">{{movimientos.length}}</div><div class="l">📋 Transacciones</div></div> | |
| </div> | |
| <nav> | |
| <div v-for="t in tabs" :key="t.id" class="ntab" :class="{on:tab===t.id}" @click="tab=t.id">{{t.label}}</div> | |
| </nav> | |
| <transition name="fade" mode="out-in"> | |
| <!-- DASHBOARD --> | |
| <div v-if="tab==='dash'" key="dash" class="pane"> | |
| <div class="card" style="margin-bottom:14px"> | |
| <h3>Cuentas Activas</h3> | |
| <table><thead><tr><th>Cuenta</th><th>Cliente</th><th>Tipo</th><th>Saldo MXN</th><th>Status</th></tr></thead> | |
| <tbody><tr v-for="(d,num) in cuentas" :key="num"> | |
| <td><b>{{num}}</b></td><td>{{clientes[d.cliente].nombre}}</td><td>{{d.tipo}}</td> | |
| <td>{{fmt(d.saldo)}}</td><td><span class="badge g">Activa</span></td> | |
| </tr></tbody></table> | |
| </div> | |
| <div class="card"> | |
| <h3>Últimas Transacciones</h3> | |
| <table><thead><tr><th>Folio</th><th>Fecha</th><th>Cuenta</th><th>Cliente</th><th>Tipo</th><th>Monto</th></tr></thead> | |
| <tbody><tr v-for="m in ultimos" :key="m.folio"> | |
| <td>{{m.folio}}</td><td>{{m.fecha}}</td><td>{{m.cuenta}}</td><td>{{m.cliente}}</td> | |
| <td><span :class="['badge',tipoBadge(m.tipo)]">{{m.tipo}}</span></td><td>{{fmt(m.monto)}}</td> | |
| </tr></tbody></table> | |
| </div> | |
| </div> | |
| <!-- CLIENTES --> | |
| <div v-else-if="tab==='cli'" key="cli" class="pane"> | |
| <div class="grid2"> | |
| <div class="card"> | |
| <h3>Alta de Cliente</h3> | |
| <label>Razón Social</label><input v-model="fCli.nombre" placeholder="Nombre o empresa"/> | |
| <label>RFC</label><input v-model="fCli.rfc" placeholder="RFC12345AAA"/> | |
| <label>Tipo</label><select v-model="fCli.tipo"><option>Personal</option><option>PyME</option><option>Corporativo</option></select> | |
| <button class="btn" @click="altaCliente">✅ Registrar</button> | |
| <div v-if="msgCli" class="alert" :class="msgCli.ok?'ok':'err'">{{msgCli.txt}}</div> | |
| </div> | |
| <div class="card"> | |
| <h3>Directorio</h3> | |
| <table><thead><tr><th>ID</th><th>Nombre</th><th>RFC</th><th>Tipo</th><th>Status</th></tr></thead> | |
| <tbody><tr v-for="(d,id) in clientes" :key="id"> | |
| <td><b>{{id}}</b></td><td>{{d.nombre}}</td><td>{{d.rfc}}</td><td>{{d.tipo}}</td> | |
| <td><span class="badge g">Activo</span></td> | |
| </tr></tbody></table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CUENTAS --> | |
| <div v-else-if="tab==='cta'" key="cta" class="pane"> | |
| <div class="grid2"> | |
| <div class="card"> | |
| <h3>Apertura de Cuenta</h3> | |
| <label>ID Cliente</label><input v-model="fCta.cliID" placeholder="CLI001"/> | |
| <label>Tipo</label><select v-model="fCta.tipo"><option>Corriente</option><option>Ahorro</option><option>Inversión</option><option>Nómina</option></select> | |
| <label>Saldo Inicial</label><input v-model.number="fCta.saldo" type="number"/> | |
| <button class="btn" @click="apertura">✅ Abrir Cuenta</button> | |
| <div v-if="msgCta" class="alert" :class="msgCta.ok?'ok':'err'">{{msgCta.txt}}</div> | |
| </div> | |
| <div class="card"> | |
| <h3>Cuentas Registradas</h3> | |
| <table><thead><tr><th>Cuenta</th><th>Cliente</th><th>Tipo</th><th>Saldo</th><th>Status</th></tr></thead> | |
| <tbody><tr v-for="(d,num) in cuentas" :key="num"> | |
| <td><b>{{num}}</b></td><td>{{clientes[d.cliente].nombre}}</td><td>{{d.tipo}}</td> | |
| <td>{{fmt(d.saldo)}}</td><td><span class="badge g">Activa</span></td> | |
| </tr></tbody></table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- OPERACIONES --> | |
| <div v-else-if="tab==='op'" key="op" class="pane"> | |
| <div class="card" style="max-width:680px;margin-bottom:14px"> | |
| <h3>Depósito / Retiro</h3> | |
| <div class="grid3"> | |
| <div><label>Cuenta</label><select v-model="fOp.cta"><option v-for="n in numCuentas" :key="n">{{n}}</option></select></div> | |
| <div><label>Operación</label><select v-model="fOp.op"><option>Depósito</option><option>Retiro</option></select></div> | |
| <div><label>Monto MXN</label><input v-model.number="fOp.monto" type="number"/></div> | |
| </div> | |
| <button class="btn" @click="operar">⚡ Ejecutar</button> | |
| <div v-if="msgOp" class="alert" :class="msgOp.ok?'ok':'err'">{{msgOp.txt}}</div> | |
| </div> | |
| <div class="card" style="max-width:680px;margin-bottom:14px"> | |
| <h3>Transferencia</h3> | |
| <div class="grid3"> | |
| <div><label>Origen</label><select v-model="fTr.orig"><option v-for="n in numCuentas" :key="n">{{n}}</option></select></div> | |
| <div><label>Destino</label><select v-model="fTr.dest"><option v-for="n in numCuentas" :key="n">{{n}}</option></select></div> | |
| <div><label>Monto MXN</label><input v-model.number="fTr.monto" type="number"/></div> | |
| </div> | |
| <button class="btn" @click="transferir">🔁 Transferir</button> | |
| <div v-if="msgTr" class="alert" :class="msgTr.ok?'ok':'err'">{{msgTr.txt}}</div> | |
| </div> | |
| <div class="card"> | |
| <h3>Movimientos — {{fOp.cta}}</h3> | |
| <table><thead><tr><th>Folio</th><th>Fecha</th><th>Tipo</th><th>Monto</th><th>Status</th></tr></thead> | |
| <tbody><tr v-for="m in movCta" :key="m.folio"> | |
| <td>{{m.folio}}</td><td>{{m.fecha}}</td> | |
| <td><span :class="['badge',tipoBadge(m.tipo)]">{{m.tipo}}</span></td> | |
| <td>{{fmt(m.monto)}}</td><td><span class="badge g">{{m.status}}</span></td> | |
| </tr></tbody></table> | |
| </div> | |
| </div> | |
| <!-- ESTADO --> | |
| <div v-else-if="tab==='ec'" key="ec" class="pane"> | |
| <div class="card" style="max-width:560px"> | |
| <h3>Estado de Cuenta</h3> | |
| <label>Cuenta</label><select v-model="fEc.cta"><option v-for="n in numCuentas" :key="n">{{n}}</option></select> | |
| <button class="btn" @click="generarEc">📄 Generar</button> | |
| </div> | |
| <div v-if="ecTxt" class="ec">{{ecTxt}}</div> | |
| </div> | |
| <!-- HISTORIAL --> | |
| <div v-else-if="tab==='hist'" key="hist" class="pane"> | |
| <div class="card"> | |
| <h3>Historial Global</h3> | |
| <table><thead><tr><th>Folio</th><th>Fecha</th><th>Cuenta</th><th>Cliente</th><th>Tipo</th><th>Monto</th><th>Status</th></tr></thead> | |
| <tbody><tr v-for="m in historial" :key="m.folio+m.tipo"> | |
| <td>{{m.folio}}</td><td>{{m.fecha}}</td><td>{{m.cuenta}}</td><td>{{m.cliente}}</td> | |
| <td><span :class="['badge',tipoBadge(m.tipo)]">{{m.tipo}}</span></td> | |
| <td>{{fmt(m.monto)}}</td><td><span class="badge g">{{m.status}}</span></td> | |
| </tr></tbody></table> | |
| </div> | |
| </div> | |
| </transition> | |
| </div> | |
| <script> | |
| const {createApp,reactive,computed,ref,onMounted} = Vue; | |
| createApp({ | |
| setup(){ | |
| const tab = ref('dash'); | |
| const clock = ref(''); | |
| const tabs = [ | |
| {id:'dash',label:'📊 Dashboard'},{id:'cli',label:'👥 Clientes'}, | |
| {id:'cta',label:'🏦 Cuentas'},{id:'op',label:'💳 Operaciones'}, | |
| {id:'ec',label:'📄 Estado'},{id:'hist',label:'📋 Historial'} | |
| ]; | |
| const clientes = reactive({ | |
| CLI001:{nombre:"Banco Nacional MX", rfc:"BNM900101XXX",tipo:"Corporativo"}, | |
| CLI002:{nombre:"Grupo Financiero Alfa", rfc:"GFA850615YYY",tipo:"Corporativo"}, | |
| CLI003:{nombre:"María González López", rfc:"GOLM780320ZZZ",tipo:"Personal"}, | |
| CLI004:{nombre:"Constructora del Norte",rfc:"CNO920801AAA",tipo:"PyME"}, | |
| }); | |
| const cuentas = reactive({ | |
| "4001-0001":{cliente:"CLI001",tipo:"Corriente", saldo:4250000, moneda:"MXN"}, | |
| "4001-0002":{cliente:"CLI001",tipo:"Inversión", saldo:12800000,moneda:"MXN"}, | |
| "4001-0003":{cliente:"CLI002",tipo:"Corriente", saldo:875300, moneda:"MXN"}, | |
| "4001-0004":{cliente:"CLI003",tipo:"Ahorro", saldo:52400, moneda:"MXN"}, | |
| "4001-0005":{cliente:"CLI004",tipo:"Corriente", saldo:320000, moneda:"MXN"}, | |
| }); | |
| const movimientos = reactive([]); | |
| let counter = 1000; | |
| const tipos = ["Depósito","Retiro","Transferencia"]; | |
| Object.keys(cuentas).forEach(cta=>{ | |
| for(let i=0;i<6;i++){ | |
| const d=new Date(); d.setDate(d.getDate()-Math.floor(Math.random()*90)); | |
| movimientos.push({ | |
| folio:`TXN${counter++}`, | |
| fecha:d.toISOString().slice(0,16).replace('T',' '), | |
| cuenta:cta, cliente:clientes[cuentas[cta].cliente].nombre, | |
| tipo:tipos[Math.floor(Math.random()*3)], | |
| monto:+(Math.random()*79000+1000).toFixed(2), status:"Completado" | |
| }); | |
| } | |
| }); | |
| const fmt = n=>'$'+n.toLocaleString('es-MX',{minimumFractionDigits:2}); | |
| const ts = ()=>new Date().toISOString().slice(0,19).replace('T',' '); | |
| const tipoBadge = t=>({Depósito:'b','Transferencia entrada':'b',Retiro:'r','Transferencia salida':'r',Transferencia:'y'}[t]||'y'); | |
| const totalClientes = computed(()=>Object.keys(clientes).length); | |
| const totalCuentas = computed(()=>Object.keys(cuentas).length); | |
| const totalSaldo = computed(()=>Object.values(cuentas).reduce((a,c)=>a+c.saldo,0)); | |
| const numCuentas = computed(()=>Object.keys(cuentas)); | |
| const ultimos = computed(()=>[...movimientos].sort((a,b)=>b.fecha.localeCompare(a.fecha)).slice(0,8)); | |
| const historial = computed(()=>[...movimientos].sort((a,b)=>b.fecha.localeCompare(a.fecha)).slice(0,40)); | |
| // forms | |
| const fCli = reactive({nombre:'',rfc:'',tipo:'Personal'}); | |
| const fCta = reactive({cliID:'',tipo:'Corriente',saldo:0}); | |
| const fOp = reactive({cta:'4001-0001',op:'Depósito',monto:1000}); | |
| const fTr = reactive({orig:'4001-0001',dest:'4001-0002',monto:5000}); | |
| const fEc = reactive({cta:'4001-0001'}); | |
| const msgCli = ref(null), msgCta = ref(null), msgOp = ref(null), msgTr = ref(null); | |
| const ecTxt = ref(''); | |
| const showMsg = (ref,txt,ok)=>{ref.value={txt,ok};setTimeout(()=>ref.value=null,4000)}; | |
| const movCta = computed(()=> | |
| [...movimientos].filter(m=>m.cuenta===fOp.cta) | |
| .sort((a,b)=>b.fecha.localeCompare(a.fecha)).slice(0,15) | |
| ); | |
| function altaCliente(){ | |
| if(!fCli.nombre||!fCli.rfc){showMsg(msgCli,'⚠️ Nombre y RFC obligatorios.',false);return} | |
| const id='CLI'+String(Object.keys(clientes).length+1).padStart(3,'0'); | |
| clientes[id]={nombre:fCli.nombre,rfc:fCli.rfc,tipo:fCli.tipo}; | |
| fCli.nombre='';fCli.rfc=''; | |
| showMsg(msgCli,`✅ Cliente ${id} registrado.`,true); | |
| } | |
| function apertura(){ | |
| if(!clientes[fCta.cliID]){showMsg(msgCta,'⚠️ ID de cliente no encontrado.',false);return} | |
| const num='4001-'+String(Object.keys(cuentas).length+1).padStart(4,'0'); | |
| cuentas[num]={cliente:fCta.cliID,tipo:fCta.tipo,saldo:fCta.saldo||0,moneda:'MXN'}; | |
| showMsg(msgCta,`✅ Cuenta ${num} abierta.`,true); | |
| } | |
| function operar(){ | |
| const c=cuentas[fOp.cta],m=fOp.monto; | |
| if(!m||m<=0){showMsg(msgOp,'⚠️ Monto inválido.',false);return} | |
| if(fOp.op==='Retiro'&&c.saldo<m){showMsg(msgOp,`⚠️ Saldo insuficiente: ${fmt(c.saldo)}`,false);return} | |
| c.saldo+=fOp.op==='Depósito'?m:-m; | |
| const folio='TXN'+counter++; | |
| movimientos.push({folio,fecha:ts(),cuenta:fOp.cta,cliente:clientes[c.cliente].nombre,tipo:fOp.op,monto:m,status:'Completado'}); | |
| showMsg(msgOp,`✅ ${fOp.op} ${fmt(m)} | Folio:${folio} | Saldo:${fmt(c.saldo)}`,true); | |
| } | |
| function transferir(){ | |
| const {orig,dest,monto}=fTr; | |
| if(orig===dest){showMsg(msgTr,'⚠️ Origen y destino iguales.',false);return} | |
| if(!monto||monto<=0){showMsg(msgTr,'⚠️ Monto inválido.',false);return} | |
| if(cuentas[orig].saldo<monto){showMsg(msgTr,`⚠️ Saldo insuficiente en ${orig}.`,false);return} | |
| cuentas[orig].saldo-=monto; cuentas[dest].saldo+=monto; | |
| const folio='TXN'+counter++; | |
| movimientos.push({folio,fecha:ts(),cuenta:orig,cliente:clientes[cuentas[orig].cliente].nombre,tipo:'Transferencia salida',monto,status:'Completado'}); | |
| movimientos.push({folio,fecha:ts(),cuenta:dest,cliente:clientes[cuentas[dest].cliente].nombre,tipo:'Transferencia entrada',monto,status:'Completado'}); | |
| showMsg(msgTr,`✅ ${orig}→${dest} | ${fmt(monto)} | Folio:${folio}`,true); | |
| } | |
| function generarEc(){ | |
| const cta=fEc.cta, d=cuentas[cta], cli=clientes[d.cliente]; | |
| const movs=[...movimientos].filter(m=>m.cuenta===cta).slice(-5).reverse(); | |
| let t=`═══════════════════════════════════════\n ESTADO DE CUENTA AS400/DB2\n═══════════════════════════════════════\n`; | |
| t+=` Cuenta : ${cta}\n Cliente : ${cli.nombre}\n RFC : ${cli.rfc}\n`; | |
| t+=` Tipo : ${d.tipo} | ${d.moneda}\n Saldo : ${fmt(d.saldo)}\n`; | |
| t+=`───────────────────────────────────────\n ÚLTIMOS MOVIMIENTOS:\n`; | |
| movs.forEach(m=>{const s=m.tipo.includes('Depósito')||m.tipo.includes('entrada')?'+':'-';t+=` ${m.fecha} ${s}${fmt(m.monto)} ${m.tipo}\n`}); | |
| t+=`═══════════════════════════════════════`; | |
| ecTxt.value=t; | |
| } | |
| onMounted(()=>{ | |
| setInterval(()=>{clock.value=new Date().toLocaleTimeString('es-MX')},1000); | |
| }); | |
| return {tab,tabs,clock,clientes,cuentas,movimientos,fmt,tipoBadge, | |
| totalClientes,totalCuentas,totalSaldo,numCuentas,ultimos,historial,movCta, | |
| fCli,fCta,fOp,fTr,fEc,msgCli,msgCta,msgOp,msgTr,ecTxt, | |
| altaCliente,apertura,operar,transferir,generarEc}; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |