|
|
|
@@ -110,6 +110,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div><label class="label">Notas</label><textarea x-model="form.notas" class="input-field w-full" rows="2"></textarea></div>
|
|
|
|
|
<div><label class="label">Chat ID de Telegram</label><input x-model="form.telegram_chat_id" class="input-field w-full font-mono" placeholder="Ej: 123456789"><p class="text-xs text-slate-400 mt-1">Se usa para enviar notificaciones al usuario vía Telegram.</p></div>
|
|
|
|
|
<div><label class="label">Sitio Web</label><input x-model="form.sitio_web" class="input-field w-full" type="url" placeholder="https://ejemplo.com"><p class="text-xs text-slate-400 mt-1">Sitio web del partner o cliente.</p></div>
|
|
|
|
|
</div>
|
|
|
|
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
|
|
|
|
<div class="flex justify-end gap-3 mt-5">
|
|
|
|
@@ -150,6 +151,37 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Modal enviar credenciales -->
|
|
|
|
|
<div x-show="showCredModal" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center bg-black/40">
|
|
|
|
|
<div @click.outside="showCredModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
|
|
|
<div class="w-10 h-10 rounded-full bg-[#8eb02f]/15 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
<svg class="w-5 h-5 text-[#6d8c24]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-base font-bold text-slate-800">Enviar credenciales de acceso</h2>
|
|
|
|
|
<p class="text-xs text-slate-500" x-text="credEmail"></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-sm text-slate-600 mb-4">Se enviará un correo de bienvenida a <strong x-text="credNombre"></strong> con el enlace al portal y sus datos de inicio de sesión.</p>
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<label class="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Contraseña a incluir en el correo</label>
|
|
|
|
|
<input x-model="credPassword" type="text" placeholder="Déjalo vacío para omitirla"
|
|
|
|
|
class="input-field w-full font-mono">
|
|
|
|
|
<p class="text-xs text-slate-400 mt-1">Si la dejas vacía se indicará al usuario que use la contraseña que configuraste.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p x-show="credMsg" x-text="credMsg"
|
|
|
|
|
:class="credOk ? 'text-green-600 bg-green-50 border border-green-200' : 'text-red-600 bg-red-50 border border-red-200'"
|
|
|
|
|
class="text-sm rounded-lg px-3 py-2 mb-3"></p>
|
|
|
|
|
<div class="flex justify-end gap-3">
|
|
|
|
|
<button @click="showCredModal=false" class="btn-secondary">Cancelar</button>
|
|
|
|
|
<button @click="enviarCredenciales()" :disabled="credSending"
|
|
|
|
|
class="btn-primary flex items-center gap-2"
|
|
|
|
|
x-text="credSending ? 'Enviando...' : 'Enviar correo'"></button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Panel detalle usuario (slide derecho) -->
|
|
|
|
|
<div x-show="showDetail" x-cloak class="fixed inset-0 z-50 flex justify-end">
|
|
|
|
|
<div class="absolute inset-0 bg-black/40" @click="showDetail=false"></div>
|
|
|
|
@@ -194,54 +226,100 @@
|
|
|
|
|
<span x-show="detailUser?.role" class="badge badge-slate" x-text="detailUser?.role?.display_name || detailUser?.role?.name"></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Campos -->
|
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
|
|
|
<div x-show="detailUser?.telefono || detailUser?.indicativo" class="col-span-2">
|
|
|
|
|
<p class="label">Teléfono</p>
|
|
|
|
|
<p class="text-sm text-slate-700" x-text="(detailUser?.indicativo || '') + ' ' + (detailUser?.telefono || '')"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-show="detailUser?.pais">
|
|
|
|
|
<p class="label">País</p>
|
|
|
|
|
<p class="text-sm text-slate-700" x-text="detailUser?.pais"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-show="detailUser?.empresa">
|
|
|
|
|
<p class="label">Empresa</p>
|
|
|
|
|
<p class="text-sm text-slate-700" x-text="detailUser?.empresa"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-show="detailUser?.documento" class="col-span-2">
|
|
|
|
|
<p class="label">NIT / Documento</p>
|
|
|
|
|
<p class="text-sm text-slate-700 font-mono" x-text="detailUser?.documento"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-show="detailUser?.telegram_chat_id" class="col-span-2">
|
|
|
|
|
<p class="label">Telegram Chat ID</p>
|
|
|
|
|
<p class="text-sm text-slate-700 font-mono" x-text="detailUser?.telegram_chat_id"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-show="detailUser?.notas" class="col-span-2">
|
|
|
|
|
<p class="label">Notas</p>
|
|
|
|
|
<p class="text-sm text-slate-500 whitespace-pre-wrap" x-text="detailUser?.notas"></p>
|
|
|
|
|
<!-- Sección: Contacto -->
|
|
|
|
|
<div class="pt-2">
|
|
|
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Contacto</p>
|
|
|
|
|
<div class="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
|
|
|
<div class="col-span-2">
|
|
|
|
|
<p class="label">Teléfono</p>
|
|
|
|
|
<p class="text-sm text-slate-700"
|
|
|
|
|
x-text="(detailUser?.indicativo || '') + ' ' + (detailUser?.telefono || '') || '—'"
|
|
|
|
|
:class="!(detailUser?.telefono) && 'text-slate-300'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="label">País</p>
|
|
|
|
|
<p class="text-sm" :class="detailUser?.pais ? 'text-slate-700' : 'text-slate-300'"
|
|
|
|
|
x-text="detailUser?.pais || '—'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="label">Sitio Web</p>
|
|
|
|
|
<template x-if="detailUser?.sitio_web">
|
|
|
|
|
<a :href="detailUser.sitio_web" target="_blank" rel="noopener noreferrer"
|
|
|
|
|
class="text-sm text-[#8eb02f] hover:underline break-all" x-text="detailUser.sitio_web"></a>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="!detailUser?.sitio_web">
|
|
|
|
|
<p class="text-sm text-slate-300">—</p>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Documento RUT (archivo) -->
|
|
|
|
|
<div x-show="detailUser?.documento_rut_file" class="pt-3 border-t border-slate-100">
|
|
|
|
|
<p class="label">Documento RUT / Cámara de comercio</p>
|
|
|
|
|
<div class="flex items-center gap-2 mt-1">
|
|
|
|
|
<svg class="w-4 h-4 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
|
|
|
<span class="text-sm text-slate-600 truncate" x-text="detailUser?.documento_rut_nombre || 'Archivo RUT'"></span>
|
|
|
|
|
<a :href="`/app/portal-usuarios/${detailUser?.ID}/rut-file`" target="_blank"
|
|
|
|
|
class="text-[#8eb02f] text-xs font-semibold hover:underline flex-shrink-0">Descargar</a>
|
|
|
|
|
<!-- Sección: Empresa / Legal -->
|
|
|
|
|
<div class="pt-3 border-t border-slate-100">
|
|
|
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Empresa / Legal</p>
|
|
|
|
|
<div class="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="label">Empresa</p>
|
|
|
|
|
<p class="text-sm" :class="detailUser?.empresa ? 'text-slate-700' : 'text-slate-300'"
|
|
|
|
|
x-text="detailUser?.empresa || '—'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="label">NIT / Documento</p>
|
|
|
|
|
<p class="text-sm font-mono" :class="detailUser?.documento ? 'text-slate-700' : 'text-slate-300'"
|
|
|
|
|
x-text="detailUser?.documento || '—'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-show="detailUser?.rol==='cliente'" class="col-span-2">
|
|
|
|
|
<p class="label">Cliente asignado</p>
|
|
|
|
|
<p class="text-sm" :class="detailUser?.cliente ? 'text-slate-700' : 'text-slate-300'"
|
|
|
|
|
x-text="detailUser?.cliente?.empresa || detailUser?.cliente?.nombre || '—'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Accesos partner -->
|
|
|
|
|
<!-- Sección: Canales -->
|
|
|
|
|
<div class="pt-3 border-t border-slate-100">
|
|
|
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Canales</p>
|
|
|
|
|
<div class="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
|
|
|
<div class="col-span-2">
|
|
|
|
|
<p class="label">Telegram Chat ID</p>
|
|
|
|
|
<p class="text-sm font-mono" :class="detailUser?.telegram_chat_id ? 'text-slate-700' : 'text-slate-300'"
|
|
|
|
|
x-text="detailUser?.telegram_chat_id || '—'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sección: Notas -->
|
|
|
|
|
<div class="pt-3 border-t border-slate-100">
|
|
|
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Notas internas</p>
|
|
|
|
|
<p class="text-sm whitespace-pre-wrap" :class="detailUser?.notas ? 'text-slate-600' : 'text-slate-300'"
|
|
|
|
|
x-text="detailUser?.notas || 'Sin notas'"></p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sección: Documento RUT -->
|
|
|
|
|
<div class="pt-3 border-t border-slate-100">
|
|
|
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Documento RUT / Cámara de comercio</p>
|
|
|
|
|
<template x-if="detailUser?.documento_rut_file">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<svg class="w-4 h-4 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
|
|
|
<span class="text-sm text-slate-600 truncate" x-text="detailUser?.documento_rut_nombre || 'Archivo RUT'"></span>
|
|
|
|
|
<a :href="`/app/portal-usuarios/${detailUser?.ID}/rut-file`" target="_blank"
|
|
|
|
|
class="text-[#8eb02f] text-xs font-semibold hover:underline flex-shrink-0">Descargar</a>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="!detailUser?.documento_rut_file">
|
|
|
|
|
<p class="text-sm text-slate-300">Sin archivo adjunto</p>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sección: Accesos partner -->
|
|
|
|
|
<div x-show="detailUser?.rol==='partner'" class="pt-3 border-t border-slate-100">
|
|
|
|
|
<p class="label">Accesos de cliente</p>
|
|
|
|
|
<div class="flex flex-wrap gap-1 mt-1">
|
|
|
|
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Accesos de cliente</p>
|
|
|
|
|
<div class="flex flex-wrap gap-1">
|
|
|
|
|
<template x-for="acc in (detailUser?.portal_accesos || [])" :key="acc.ID">
|
|
|
|
|
<span class="bg-slate-100 text-slate-600 px-2 py-0.5 rounded text-xs"
|
|
|
|
|
x-text="acc.cliente?.empresa || acc.cliente?.nombre || acc.cliente_id"></span>
|
|
|
|
|
</template>
|
|
|
|
|
<span x-show="!(detailUser?.portal_accesos?.length)" class="text-slate-400 text-xs">Sin accesos asignados</span>
|
|
|
|
|
<span x-show="!(detailUser?.portal_accesos?.length)" class="text-slate-300 text-xs">Sin accesos asignados</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -252,8 +330,12 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Footer -->
|
|
|
|
|
<div x-show="detailUser && !loadingDetail" class="flex-shrink-0 px-5 py-4 border-t border-slate-200 bg-slate-50 flex gap-2">
|
|
|
|
|
<div x-show="detailUser && !loadingDetail" class="flex-shrink-0 px-5 py-4 border-t border-slate-200 bg-slate-50 flex gap-2 flex-wrap">
|
|
|
|
|
<button @click="openEdit(detailUser); showDetail=false;" class="btn-secondary flex-1">Editar</button>
|
|
|
|
|
<button @click="openCredModal(detailUser)" class="flex-1 flex items-center justify-center gap-1.5 text-sm font-semibold px-3 py-2 rounded-lg border border-[#8eb02f] text-[#6d8c24] hover:bg-[#8eb02f]/10 transition-colors">
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
|
|
|
Enviar accesos
|
|
|
|
|
</button>
|
|
|
|
|
<button @click="showDetail=false" class="btn-primary flex-1">Cerrar</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -270,7 +352,8 @@ function portalUsuariosApp() {
|
|
|
|
|
showDetail: false, detailUser: null, loadingDetail: false,
|
|
|
|
|
editId: null, deleteId: null, error: '',
|
|
|
|
|
accesoUserId: null, accesoClienteId: '',
|
|
|
|
|
form: { nombre:'', email:'', password:'', rol:'cliente', cliente_id:'', activo:true, notas:'', telegram_chat_id:'' },
|
|
|
|
|
showCredModal: false, credUserId: null, credNombre: '', credEmail: '', credPassword: '', credSending: false, credMsg: '', credOk: false,
|
|
|
|
|
form: { nombre:'', email:'', password:'', rol:'cliente', cliente_id:'', activo:true, notas:'', telegram_chat_id:'', sitio_web:'' },
|
|
|
|
|
|
|
|
|
|
async init() { await this.load(); },
|
|
|
|
|
|
|
|
|
@@ -299,25 +382,57 @@ function portalUsuariosApp() {
|
|
|
|
|
|
|
|
|
|
openCreate() {
|
|
|
|
|
this.editId=null; this.error='';
|
|
|
|
|
this.form={nombre:'',email:'',password:'',rol:'cliente',cliente_id:'',activo:true,notas:'',telegram_chat_id:''};
|
|
|
|
|
this.form={nombre:'',email:'',password:'',rol:'cliente',cliente_id:'',activo:true,notas:'',telegram_chat_id:'',sitio_web:''};
|
|
|
|
|
this.showModal=true;
|
|
|
|
|
},
|
|
|
|
|
openEdit(u) {
|
|
|
|
|
this.editId=u.ID; this.error='';
|
|
|
|
|
this.form={nombre:u.nombre,email:u.email,password:'',rol:u.rol,cliente_id:u.cliente_id||'',activo:u.activo,notas:u.notas||'',telegram_chat_id:u.telegram_chat_id||''};
|
|
|
|
|
this.form={nombre:u.nombre,email:u.email,password:'',rol:u.rol,cliente_id:u.cliente_id||'',activo:u.activo,notas:u.notas||'',telegram_chat_id:u.telegram_chat_id||'',sitio_web:u.sitio_web||''};
|
|
|
|
|
this.showModal=true;
|
|
|
|
|
},
|
|
|
|
|
async save() {
|
|
|
|
|
this.saving=true; this.error='';
|
|
|
|
|
const payload={...this.form};
|
|
|
|
|
const plainPassword = payload.password;
|
|
|
|
|
if(payload.cliente_id==='') payload.cliente_id=null;
|
|
|
|
|
try {
|
|
|
|
|
if(this.editId) await axios.put(`/app/portal-usuarios/${this.editId}`, payload);
|
|
|
|
|
else await axios.post('/app/portal-usuarios', payload);
|
|
|
|
|
this.showModal=false; await this.load();
|
|
|
|
|
if(this.editId) {
|
|
|
|
|
await axios.put(`/app/portal-usuarios/${this.editId}`, payload);
|
|
|
|
|
this.showModal=false;
|
|
|
|
|
} else {
|
|
|
|
|
const r = await axios.post('/app/portal-usuarios', payload);
|
|
|
|
|
this.showModal=false;
|
|
|
|
|
// Ofrecer envío de credenciales al crear
|
|
|
|
|
this.credUserId = r.data.ID;
|
|
|
|
|
this.credNombre = r.data.nombre;
|
|
|
|
|
this.credEmail = r.data.email;
|
|
|
|
|
this.credPassword = plainPassword;
|
|
|
|
|
this.credMsg = ''; this.credOk = false;
|
|
|
|
|
this.showCredModal = true;
|
|
|
|
|
}
|
|
|
|
|
await this.load();
|
|
|
|
|
} catch(e) { this.error=e.response?.data?.error||'Error al guardar'; }
|
|
|
|
|
finally { this.saving=false; }
|
|
|
|
|
},
|
|
|
|
|
openCredModal(u) {
|
|
|
|
|
this.credUserId = u.ID;
|
|
|
|
|
this.credNombre = u.nombre;
|
|
|
|
|
this.credEmail = u.email;
|
|
|
|
|
this.credPassword = '';
|
|
|
|
|
this.credMsg = ''; this.credOk = false;
|
|
|
|
|
this.showCredModal = true;
|
|
|
|
|
},
|
|
|
|
|
async enviarCredenciales() {
|
|
|
|
|
this.credSending = true; this.credMsg = '';
|
|
|
|
|
try {
|
|
|
|
|
const r = await axios.post(`/app/portal-usuarios/${this.credUserId}/send-credentials`, { password: this.credPassword });
|
|
|
|
|
this.credMsg = '✅ ' + (r.data.message || 'Correo enviado correctamente.');
|
|
|
|
|
this.credOk = true;
|
|
|
|
|
} catch(e) {
|
|
|
|
|
this.credMsg = e.response?.data?.error || 'Error al enviar el correo.';
|
|
|
|
|
this.credOk = false;
|
|
|
|
|
} finally { this.credSending = false; }
|
|
|
|
|
},
|
|
|
|
|
confirmDelete(u) { this.deleteId=u.ID; this.showDelete=true; },
|
|
|
|
|
async doDelete() {
|
|
|
|
|
this.saving=true;
|
|
|
|
|