up
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<!-- Portal Dashboard -->
|
||||
<div x-data="portalDashboard()" x-init="init()">
|
||||
|
||||
<!-- Saludo -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-800">
|
||||
Hola, {{ if .portalUser }}{{ .portalUser.Nombre }}{{ end }} 👋
|
||||
</h1>
|
||||
<p class="text-slate-500 text-sm mt-1">
|
||||
{{ if .isPartner }}
|
||||
Estás viendo todos los proyectos de tus clientes.
|
||||
{{ else }}
|
||||
Aquí puedes ver el avance de tus proyectos.
|
||||
{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Proyectos cards -->
|
||||
{{ if .isPartner }}
|
||||
<!-- Agrupado por cliente -->
|
||||
{{ range .grupos }}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-sm font-bold text-slate-500 uppercase tracking-wide mb-3">
|
||||
{{ if .Cliente.RazonSocial }}{{ .Cliente.RazonSocial }}{{ else }}{{ .Cliente.Nombre }}{{ end }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{{ range .Proyectos }}
|
||||
<a href="/portal/proyecto/{{ .Slug }}" class="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow p-5 block group">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="w-3 h-3 rounded-full" style="background:{{ .Color }}"></span>
|
||||
<span class="font-semibold text-slate-800 group-hover:text-[#8eb02f] transition-colors text-sm">{{ .Nombre }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mb-3 line-clamp-2">{{ .Descripcion }}</p>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-medium text-slate-500">Progreso</span>
|
||||
<span class="text-xs font-bold text-slate-700">{{ .Progreso }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 rounded-full h-2">
|
||||
<div class="h-2 rounded-full" style="background:{{ .Color }};width:{{ .Progreso }}%"></div>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-between items-center">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full font-semibold" style="background:#f1f5f9;color:#475569">{{ .Estado }}</span>
|
||||
<span class="text-xs text-slate-400">Ver detalle →</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<!-- Vista simple para cliente -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{{ range .proyectos }}
|
||||
<a href="/portal/proyecto/{{ .Slug }}" class="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow p-5 block group">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="w-3 h-3 rounded-full" style="background:{{ .Color }}"></span>
|
||||
<span class="font-semibold text-slate-800 group-hover:text-[#8eb02f] transition-colors text-sm">{{ .Nombre }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mb-3 line-clamp-2">{{ .Descripcion }}</p>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-medium text-slate-500">Progreso</span>
|
||||
<span class="text-xs font-bold text-slate-700">{{ .Progreso }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 rounded-full h-2">
|
||||
<div class="h-2 rounded-full" style="background:{{ .Color }};width:{{ .Progreso }}%"></div>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-between items-center">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full font-semibold" style="background:#f1f5f9;color:#475569">{{ .Estado }}</span>
|
||||
<span class="text-xs text-slate-400">Ver detalle →</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if not .proyectos }}
|
||||
<div class="text-center py-20 text-slate-400">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"/></svg>
|
||||
<p>Aún no tienes proyectos asignados.</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!-- Portal Login -->
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<span class="w-14 h-14 rounded-2xl flex items-center justify-center text-white text-2xl font-bold mx-auto mb-4" style="background:#8eb02f">U</span>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Portal de Clientes</h1>
|
||||
<p class="text-slate-500 text-sm mt-1">Accede a tu espacio de trabajo</p>
|
||||
</div>
|
||||
|
||||
{{ if .error }}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 rounded-xl px-4 py-3 mb-4 text-sm">
|
||||
{{ .error }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-slate-200 p-8">
|
||||
<form method="POST" action="/portal/login">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Email</label>
|
||||
<input type="email" name="email" required autofocus
|
||||
class="w-full border border-slate-200 rounded-xl px-4 py-3 text-sm outline-none focus:border-[#8eb02f] transition-colors"
|
||||
placeholder="tu@email.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Contraseña</label>
|
||||
<input type="password" name="password" required
|
||||
class="w-full border border-slate-200 rounded-xl px-4 py-3 text-sm outline-none focus:border-[#8eb02f] transition-colors">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full py-3 rounded-xl font-semibold text-white text-sm transition-colors"
|
||||
style="background:#8eb02f"
|
||||
onmouseover="this.style.background='#6d8c24'"
|
||||
onmouseout="this.style.background='#8eb02f'">
|
||||
Ingresar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-slate-400 mt-6">
|
||||
© 2025 U-site — Todos los derechos reservados
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,294 @@
|
||||
<!-- Portal Proyecto Detalle -->
|
||||
<div x-data="portalProyecto()" x-init="init()">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<a href="/portal/dashboard" class="text-slate-400 hover:text-slate-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
|
||||
</a>
|
||||
<span class="w-4 h-4 rounded-full border" style="background:{{ .proyecto.Color }}"></span>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-slate-800">{{ .proyecto.Nombre }}</h1>
|
||||
<p class="text-xs text-slate-400">{{ .proyecto.Descripcion }}</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-32 bg-slate-200 rounded-full h-2">
|
||||
<div class="h-2 rounded-full" style="background:{{ .proyecto.Color }};width:{{ .proyecto.Progreso }}%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-700">{{ .proyecto.Progreso }}%</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1 text-right capitalize">{{ .proyecto.Estado }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-slate-200 mb-6 gap-0 overflow-x-auto">
|
||||
<template x-for="tab in ['Roadmap','Avances','Entregables','Tickets','Facturas']" :key="tab">
|
||||
<button @click="activeTab=tab" class="px-4 py-2 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors"
|
||||
:class="activeTab===tab ? 'font-semibold border-[#8eb02f] text-[#8eb02f]' : 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||
x-text="tab"></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ─── ROADMAP ────────────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Roadmap'" x-cloak>
|
||||
<div class="relative">
|
||||
<!-- Línea vertical -->
|
||||
<div class="absolute left-6 top-0 bottom-0 w-0.5 bg-slate-200"></div>
|
||||
|
||||
<div class="space-y-0">
|
||||
{{ range $i, $f := .fases }}
|
||||
<div class="relative flex items-start gap-4 pb-6">
|
||||
<!-- Dot -->
|
||||
<div class="relative z-10 flex-shrink-0">
|
||||
{{ if eq $f.Estado "completado" }}
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-sm" style="background:#8eb02f">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
|
||||
</div>
|
||||
{{ else if eq $f.Estado "en_progreso" }}
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center font-bold text-sm border-2 border-[#8eb02f] text-[#8eb02f] bg-white">
|
||||
{{ add $i 1 }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center font-bold text-sm border-2 border-slate-200 text-slate-400 bg-white">
|
||||
{{ add $i 1 }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<!-- Contenido -->
|
||||
<div class="flex-1 bg-white rounded-xl border border-slate-200 p-4 mt-1">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h3 class="font-semibold text-slate-800 text-sm">{{ $f.Nombre }}</h3>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full capitalize font-medium
|
||||
{{ if eq $f.Estado "completado" }}bg-green-100 text-green-700
|
||||
{{ else if eq $f.Estado "en_progreso" }}bg-yellow-100 text-yellow-700
|
||||
{{ else if eq $f.Estado "bloqueado" }}bg-red-100 text-red-700
|
||||
{{ else }}bg-slate-100 text-slate-500{{ end }}">
|
||||
{{ $f.Estado }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">{{ $f.Descripcion }}</p>
|
||||
</div>
|
||||
{{ if $f.FechaEstimada }}
|
||||
<span class="text-xs text-slate-400 flex-shrink-0 ml-3">
|
||||
Est: {{ $f.FechaEstimada.Format "02 Jan 2006" }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if not .fases }}
|
||||
<p class="text-slate-400 text-sm text-center py-10">El roadmap aún no está disponible.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── AVANCES ───────────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Avances'" x-cloak>
|
||||
<div class="space-y-4">
|
||||
{{ range .avances }}
|
||||
<div class="bg-white rounded-xl border border-slate-200 p-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 rounded-full flex-shrink-0 flex items-center justify-center text-xs font-bold"
|
||||
style="background:#8eb02f;color:#fff">
|
||||
{{ if eq .Tipo "milestone" }}🎯{{ else if eq .Tipo "alerta" }}⚠️{{ else }}✓{{ end }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-800 text-sm">{{ .Titulo }}</h3>
|
||||
<p class="text-sm text-slate-600 mt-1">{{ .Contenido }}</p>
|
||||
<p class="text-xs text-slate-400 mt-2">{{ .CreatedAt.Format "02 Jan 2006" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if not .avances }}
|
||||
<p class="text-slate-400 text-sm text-center py-10">Sin avances publicados aún.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── ENTREGABLES ───────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Entregables'" x-cloak>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{{ range .entregables }}
|
||||
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-slate-100 flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-slate-500" 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>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-semibold text-sm text-slate-800 truncate">{{ .Nombre }}</p>
|
||||
<p class="text-xs text-slate-500">v{{ .Version }} · {{ .OriginalName }}</p>
|
||||
</div>
|
||||
<a href="/portal/entregables/{{ .ID }}/download" class="flex-shrink-0 text-[#8eb02f] hover:text-[#6d8c24]">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if not .entregables }}
|
||||
<p class="col-span-2 text-slate-400 text-sm text-center py-10">Sin entregables disponibles.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── TICKETS ───────────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Tickets'" x-cloak>
|
||||
<!-- Crear ticket -->
|
||||
<div class="bg-white rounded-xl border border-slate-200 p-5 mb-6">
|
||||
<h3 class="font-semibold text-slate-700 mb-3 text-sm">Abrir nuevo ticket</h3>
|
||||
<form @submit.prevent="crearTicket()">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
||||
<div class="sm:col-span-2"><input x-model="ticketForm.titulo" type="text" placeholder="Título del ticket" class="input-field w-full" required></div>
|
||||
<div>
|
||||
<select x-model="ticketForm.prioridad" class="input-field w-full">
|
||||
<option value="baja">Prioridad baja</option>
|
||||
<option value="media" selected>Prioridad media</option>
|
||||
<option value="alta">Prioridad alta</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2"><textarea x-model="ticketForm.descripcion" placeholder="Describe el problema o consulta..." rows="3" class="input-field w-full"></textarea></div>
|
||||
</div>
|
||||
<button type="submit" :disabled="creandoTicket" class="btn-primary text-sm" x-text="creandoTicket ? 'Enviando...' : 'Enviar ticket'"></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Lista de tickets -->
|
||||
<div class="space-y-3">
|
||||
<template x-for="t in tickets" :key="t.ID">
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div class="p-4 flex items-start justify-between cursor-pointer" @click="t._open=!t._open">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="badge" :class="ticketBadge(t.estado)" x-text="t.estado"></span>
|
||||
<span class="badge" :class="prioBadge(t.prioridad)" x-text="t.prioridad"></span>
|
||||
</div>
|
||||
<h4 class="font-semibold text-slate-800 text-sm" x-text="t.titulo"></h4>
|
||||
<p class="text-xs text-slate-400" x-text="formatDate(t.CreatedAt)"></p>
|
||||
</div>
|
||||
<svg class="w-4 h-4 text-slate-400 transition-transform" :class="t._open ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/></svg>
|
||||
</div>
|
||||
<div x-show="t._open" class="border-t border-slate-100 px-4 pb-4">
|
||||
<p class="text-sm text-slate-600 py-3 border-b border-slate-100" x-text="t.descripcion"></p>
|
||||
<div class="space-y-2 mt-3">
|
||||
<template x-for="m in (t.mensajes||[])" :key="m.ID">
|
||||
<div class="flex" :class="m.es_admin ? 'justify-start' : 'justify-end'">
|
||||
<div class="rounded-xl px-3 py-2 text-sm max-w-xs"
|
||||
:class="m.es_admin ? 'bg-slate-100 text-slate-700' : 'text-white'" :style="m.es_admin ? '' : 'background:#8eb02f'">
|
||||
<div class="text-xs font-semibold mb-0.5" x-text="m.autor_nombre" :style="m.es_admin ? 'color:#64748b' : 'color:rgba(255,255,255,.8)'"></div>
|
||||
<span x-text="m.contenido"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-if="t.estado !== 'cerrado' && t.estado !== 'resuelto'">
|
||||
<div class="flex gap-2 mt-3">
|
||||
<input x-model="t._reply" type="text" placeholder="Responder..." class="input-field flex-1 text-sm">
|
||||
<button @click="responderTicket(t)" class="btn-primary text-xs">Enviar</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="tickets.length===0 && !loadingTickets" class="text-slate-400 text-sm text-center py-6">No tienes tickets abiertos.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── FACTURAS ──────────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Facturas'" x-cloak>
|
||||
<div class="space-y-3">
|
||||
{{ range .facturas }}
|
||||
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-sm text-slate-800">{{ if .Numero }}{{ .Numero }}{{ else }}Factura #{{ .ID }}{{ end }}</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full font-semibold capitalize
|
||||
{{ if eq .Estado "pagada" }}bg-green-100 text-green-700
|
||||
{{ else if eq .Estado "vencida" }}bg-red-100 text-red-700
|
||||
{{ else if eq .Estado "cancelada" }}bg-slate-100 text-slate-500
|
||||
{{ else }}bg-yellow-100 text-yellow-700{{ end }}">{{ .Estado }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">{{ .Descripcion }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 mt-1">{{ .Moneda }} {{ .Monto }}</p>
|
||||
{{ if .FechaVencimiento }}<p class="text-xs text-slate-400">Vence: {{ .FechaVencimiento.Format "02 Jan 2006" }}</p>{{ end }}
|
||||
</div>
|
||||
{{ if .Archivo }}
|
||||
<a href="/portal/facturas/{{ .ID }}/download" class="text-[#8eb02f] hover:text-[#6d8c24]">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if not .facturas }}
|
||||
<p class="text-slate-400 text-sm text-center py-10">Sin facturas disponibles.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PROYECTO_SLUG = '{{ .proyecto.Slug }}';
|
||||
|
||||
function portalProyecto() {
|
||||
return {
|
||||
activeTab: 'Roadmap',
|
||||
tickets: [],
|
||||
loadingTickets: false,
|
||||
creandoTicket: false,
|
||||
ticketForm: { titulo:'', descripcion:'', prioridad:'media' },
|
||||
|
||||
async init() {
|
||||
await this.loadTickets();
|
||||
},
|
||||
|
||||
async loadTickets() {
|
||||
this.loadingTickets = true;
|
||||
try {
|
||||
const r = await axios.get(`/portal/api/proyecto/${PROYECTO_SLUG}`);
|
||||
this.tickets = (r.data.tickets||[]).map(t => ({...t, _open:false, _reply:''}));
|
||||
} catch(e){} finally { this.loadingTickets=false; }
|
||||
},
|
||||
|
||||
async crearTicket() {
|
||||
this.creandoTicket=true;
|
||||
try {
|
||||
const r = await axios.post('/portal/tickets', { ...this.ticketForm, proyecto_slug: PROYECTO_SLUG });
|
||||
this.tickets.unshift({...r.data, _open:false, _reply:''});
|
||||
this.ticketForm={titulo:'',descripcion:'',prioridad:'media'};
|
||||
} catch(e) {
|
||||
alert(e.response?.data?.error || 'Error al crear ticket');
|
||||
} finally { this.creandoTicket=false; }
|
||||
},
|
||||
|
||||
async responderTicket(t) {
|
||||
if(!t._reply?.trim()) return;
|
||||
try {
|
||||
const r = await axios.post(`/portal/tickets/${t.ID}/mensaje`, {contenido: t._reply});
|
||||
if(!t.mensajes) t.mensajes=[];
|
||||
t.mensajes.push(r.data);
|
||||
t._reply='';
|
||||
} catch(e) { alert(e.response?.data?.error||'Error'); }
|
||||
},
|
||||
|
||||
ticketBadge(e){ return {abierto:'badge-red',en_progreso:'badge-yellow',resuelto:'badge-green',cerrado:'badge-slate'}[e]||'badge-slate'; },
|
||||
prioBadge(p){ return {alta:'badge-red',media:'badge-yellow',baja:'badge-green'}[p]||'badge-slate'; },
|
||||
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric'}); },
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.btn-primary{background:#8eb02f;color:#fff;padding:.5rem 1rem;border-radius:.5rem;font-weight:600;font-size:.875rem;}
|
||||
.btn-primary:hover{background:#6d8c24;}
|
||||
.input-field{border:1px solid #e2e8f0;border-radius:.5rem;padding:.5rem .75rem;font-size:.875rem;outline:none;}
|
||||
.input-field:focus{border-color:#8eb02f;}
|
||||
.badge{display:inline-block;padding:.15rem .6rem;border-radius:9999px;font-size:.7rem;font-weight:600;text-transform:capitalize;}
|
||||
.badge-green{background:#dcfce7;color:#15803d;}
|
||||
.badge-yellow{background:#fef9c3;color:#854d0e;}
|
||||
.badge-slate{background:#f1f5f9;color:#475569;}
|
||||
.badge-red{background:#fee2e2;color:#991b1b;}
|
||||
</style>
|
||||
Reference in New Issue
Block a user