up
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-slate-200 mb-6 gap-0">
|
||||
<template x-for="tab in ['Fases','Avances','Entregables','Tickets']" :key="tab">
|
||||
<template x-for="tab in ['Fases','Avances','Entregables','Documentos','Tickets']" :key="tab">
|
||||
<button @click="activeTab=tab" :class="activeTab===tab ? 'border-b-2 font-semibold text-primary' : 'text-slate-500'" class="px-4 py-2 text-sm border-b-2 border-transparent -mb-px transition-colors" x-text="tab" :style="activeTab===tab ? 'border-color:#8eb02f;color:#8eb02f' : ''"></button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -117,6 +117,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Tab Documentos ────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Documentos'" x-cloak>
|
||||
<div class="flex justify-between mb-4">
|
||||
<h2 class="font-semibold text-slate-700">Contratos y órdenes de servicio</h2>
|
||||
<button @click="openDocumentoModal()" class="btn-primary text-xs">+ Subir documento</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<template x-for="d in documentos" :key="d.ID">
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4 flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-9 h-9 rounded-lg flex items-center justify-center" :style="docTipoColor(d.tipo)">
|
||||
<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="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">
|
||||
<div class="font-semibold text-sm text-slate-800" x-text="d.nombre"></div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span class="badge" :class="docBadgeClass(d.tipo)" x-text="docTipoLabel(d.tipo)"></span>
|
||||
<span class="text-xs text-slate-400" x-text="formatBytes(d.tamanio)"></span>
|
||||
<span class="text-xs text-slate-400" x-text="formatDate(d.CreatedAt)"></span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 mt-0.5" x-text="d.descripcion"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<a :href="`/app/proyectos/{{ .proyecto.ID }}/documentos/${d.ID}/download`" class="btn-icon text-blue-500" title="Descargar">
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
<button @click="deleteDocumento(d.ID)" class="btn-icon text-red-400" title="Eliminar">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="documentos.length===0" class="text-slate-400 text-sm text-center py-8">Sin documentos. Sube un contrato u orden de servicio.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Tab Tickets ───────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Tickets'" x-cloak>
|
||||
<div class="flex justify-between mb-4">
|
||||
@@ -244,6 +279,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Modal Documento ────────────────────────────────────────────────────── -->
|
||||
<div x-show="showDocumentoModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div @click.outside="showDocumentoModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||
<h2 class="text-lg font-bold mb-4">Subir documento</h2>
|
||||
<form @submit.prevent="saveDocumento()" enctype="multipart/form-data">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="label">Tipo</label>
|
||||
<select x-model="documentoForm.tipo" class="input-field w-full">
|
||||
<option value="contrato">Contrato</option>
|
||||
<option value="orden_servicio">Orden de servicio</option>
|
||||
<option value="otro">Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div><label class="label">Nombre</label><input x-model="documentoForm.nombre" class="input-field w-full" placeholder="Ej: Contrato firmado cliente"></div>
|
||||
<div><label class="label">Descripción</label><input x-model="documentoForm.descripcion" class="input-field w-full" placeholder="Opcional"></div>
|
||||
<div><label class="label">Archivo</label><input type="file" @change="documentoFile=$event.target.files[0]" class="input-field w-full" required accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.xlsx,.xls"></div>
|
||||
</div>
|
||||
<p x-show="documentoError" x-text="documentoError" class="text-red-500 text-sm mt-2"></p>
|
||||
<div class="flex justify-end gap-3 mt-4">
|
||||
<button type="button" @click="showDocumentoModal=false" class="btn-secondary">Cancelar</button>
|
||||
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Subiendo...':'Subir'"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -251,7 +313,7 @@ function proyectoDetalle(proyectoId, slug) {
|
||||
return {
|
||||
proyectoId, slug,
|
||||
activeTab: 'Fases',
|
||||
fases: [], avances: [], entregables: [], tickets: [],
|
||||
fases: [], avances: [], entregables: [], tickets: [], documentos: [],
|
||||
saving: false,
|
||||
showFaseModal: false, faseEditId: null,
|
||||
faseForm: { nombre:'', descripcion:'', estado:'pendiente', orden:1, fecha_estimada:'' },
|
||||
@@ -260,9 +322,12 @@ function proyectoDetalle(proyectoId, slug) {
|
||||
showEntregableModal: false,
|
||||
entregableForm: { nombre:'', descripcion:'', version:'1.0' },
|
||||
entregableFile: null, entregableError:'',
|
||||
showDocumentoModal: false,
|
||||
documentoForm: { tipo:'contrato', nombre:'', descripcion:'' },
|
||||
documentoFile: null, documentoError:'',
|
||||
|
||||
async init() {
|
||||
await Promise.all([this.loadFases(), this.loadAvances(), this.loadEntregables(), this.loadTickets()]);
|
||||
await Promise.all([this.loadFases(), this.loadAvances(), this.loadEntregables(), this.loadTickets(), this.loadDocumentos()]);
|
||||
},
|
||||
|
||||
async loadFases() {
|
||||
@@ -281,6 +346,10 @@ function proyectoDetalle(proyectoId, slug) {
|
||||
const r = await axios.get(`/app/proyectos/${this.proyectoId}/tickets`);
|
||||
this.tickets = (r.data || []).map(t => ({ ...t, _open: false, _reply: '' }));
|
||||
},
|
||||
async loadDocumentos() {
|
||||
const r = await axios.get(`/app/proyectos/${this.proyectoId}/documentos`);
|
||||
this.documentos = r.data || [];
|
||||
},
|
||||
|
||||
// ─── Fases ───
|
||||
openFaseModal() { this.faseEditId=null; this.faseForm={nombre:'',descripcion:'',estado:'pendiente',orden:this.fases.length+1,fecha_estimada:''}; this.showFaseModal=true; },
|
||||
@@ -353,6 +422,33 @@ function proyectoDetalle(proyectoId, slug) {
|
||||
await this.loadEntregables();
|
||||
},
|
||||
|
||||
// ─── Documentos ───
|
||||
openDocumentoModal() { this.documentoForm={tipo:'contrato',nombre:'',descripcion:''}; this.documentoFile=null; this.documentoError=''; this.showDocumentoModal=true; },
|
||||
async saveDocumento() {
|
||||
if(!this.documentoFile) { this.documentoError='Selecciona un archivo'; return; }
|
||||
this.saving=true; this.documentoError='';
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('tipo', this.documentoForm.tipo);
|
||||
fd.append('nombre', this.documentoForm.nombre);
|
||||
fd.append('descripcion', this.documentoForm.descripcion);
|
||||
fd.append('archivo', this.documentoFile);
|
||||
await axios.post(`/app/proyectos/${this.proyectoId}/documentos`, fd, { headers:{'Content-Type':'multipart/form-data'} });
|
||||
this.showDocumentoModal=false;
|
||||
await this.loadDocumentos();
|
||||
} catch(e) {
|
||||
this.documentoError = e.response?.data?.error || 'Error al subir';
|
||||
} finally { this.saving=false; }
|
||||
},
|
||||
async deleteDocumento(id) {
|
||||
if(!confirm('¿Eliminar este documento?')) return;
|
||||
await axios.delete(`/app/proyectos/${this.proyectoId}/documentos/${id}`);
|
||||
await this.loadDocumentos();
|
||||
},
|
||||
docBadgeClass(t) { return {contrato:'badge-blue',orden_servicio:'badge-green',otro:'badge-slate'}[t]||'badge-slate'; },
|
||||
docTipoLabel(t) { return {contrato:'Contrato',orden_servicio:'Orden de servicio',otro:'Otro'}[t]||t; },
|
||||
docTipoColor(t) { const m={contrato:'background:#dbeafe;color:#1d4ed8',orden_servicio:'background:#dcfce7;color:#15803d',otro:'background:#f1f5f9;color:#475569'}; return m[t]||m.otro; },
|
||||
|
||||
// ─── Tickets ───
|
||||
async cambiarEstado(t, estado) {
|
||||
await axios.put(`/app/proyectos/${this.proyectoId}/tickets/${t.ID}/estado`, {estado});
|
||||
|
||||
Reference in New Issue
Block a user