AS4000 / index.html
dimensionalpulsar's picture
Vue 3 reactive rewrite - instant tabs
d7c93a9 verified
Raw
History Blame Contribute Delete
17.6 kB
<!DOCTYPE html><!-- 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>