Asuntos Internal Implementado

This commit is contained in:
2026-03-17 11:48:15 -06:00
parent d691b71c44
commit b67a384923
2 changed files with 281 additions and 8 deletions

View File

@@ -0,0 +1,230 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { api } from '$lib/utils/api';
import { toast } from '$lib/stores/toast';
export let ticketId: string;
export let users: any[] = [];
const dispatch = createEventDispatcher();
const PRIORITIES = [
{ value: 'LOW', label: 'Baja' },
{ value: 'MEDIUM', label: 'Media' },
{ value: 'HIGH', label: 'Alta' },
{ value: 'URGENT', label: 'Urgente' }
];
let content = '';
let priority = 'MEDIUM';
let taggedUserIds: string[] = [];
let file: File | null = null;
let isSubmitting = false;
let fileInput: HTMLInputElement;
function toggleUser(userId: string) {
if (taggedUserIds.includes(userId)) {
taggedUserIds = taggedUserIds.filter(id => id !== userId);
} else {
taggedUserIds = [...taggedUserIds, userId];
}
}
function handleFileChange(e: Event) {
const input = e.target as HTMLInputElement;
file = input.files?.[0] ?? null;
}
async function handleSubmit() {
if (!content.trim()) {
toast.error('El contenido del asunto es obligatorio');
return;
}
isSubmitting = true;
try {
// Si hay archivo usamos FormData, si no JSON normal
if (file) {
const formData = new FormData();
formData.append('content', content.trim());
formData.append('priority', priority);
taggedUserIds.forEach(id => formData.append('tagged_user_ids', id));
formData.append('file', file);
await api.postForm(`/tickets/${ticketId}/issues`, formData);
} else {
await api.post(`/tickets/${ticketId}/issues`, {
content: content.trim(),
priority,
tagged_user_ids: taggedUserIds
});
}
toast.success('Asunto creado correctamente');
dispatch('created');
dispatch('close');
} catch (e) {
toast.error(e.message || 'Error al crear el asunto');
} finally {
isSubmitting = false;
}
}
function handleClose() {
dispatch('close');
}
</script>
<!-- Overlay -->
<div class="fixed inset-0 z-50 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<!-- Backdrop -->
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
on:click={handleClose}
role="button"
tabindex="-1"
on:keydown={(e) => e.key === 'Escape' && handleClose()}
/>
<!-- Modal -->
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-lg z-10">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900">Crear Asunto</h3>
<button
type="button"
on:click={handleClose}
class="text-gray-400 hover:text-gray-500"
>
<span class="sr-only">Cerrar</span>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Body -->
<div class="px-6 py-5 space-y-5">
<!-- Contenido -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Descripción del asunto <span class="text-red-500">*</span>
</label>
<textarea
rows="4"
bind:value={content}
disabled={isSubmitting}
placeholder="Describe el asunto de escalación..."
class="block w-full rounded-md border border-gray-300 shadow-sm
focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2
disabled:opacity-50"
/>
</div>
<!-- Prioridad -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Prioridad</label>
<select
bind:value={priority}
disabled={isSubmitting}
class="block w-full rounded-md border border-gray-300 shadow-sm
focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2
disabled:opacity-50"
>
{#each PRIORITIES as p}
<option value={p.value}>{p.label}</option>
{/each}
</select>
</div>
<!-- Usuarios a etiquetar -->
{#if users.length > 0}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Etiquetar usuarios
</label>
<div class="max-h-40 overflow-y-auto border border-gray-200 rounded-md divide-y divide-gray-100">
{#each users as user}
<label class="flex items-center gap-3 px-3 py-2 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={taggedUserIds.includes(user.id)}
on:change={() => toggleUser(user.id)}
disabled={isSubmitting}
class="h-4 w-4 rounded border-gray-300 text-blue-600
focus:ring-blue-500 disabled:opacity-50"
/>
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-900">
{user.first_name} {user.last_name}
</span>
<span class="text-xs text-gray-500">{user.email}</span>
</div>
</label>
{/each}
</div>
{#if taggedUserIds.length > 0}
<p class="text-xs text-blue-600 mt-1">
{taggedUserIds.length} usuario{taggedUserIds.length > 1 ? 's' : ''} seleccionado{taggedUserIds.length > 1 ? 's' : ''}
</p>
{/if}
</div>
{/if}
<!-- Adjunto -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Adjunto (opcional)
</label>
<input
bind:this={fileInput}
type="file"
on:change={handleFileChange}
disabled={isSubmitting}
class="block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-md
file:border-0 file:text-sm file:font-medium
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100 disabled:opacity-50"
/>
{#if file}
<p class="text-xs text-gray-500 mt-1">{file.name}</p>
{/if}
</div>
</div>
<!-- Footer -->
<div class="flex justify-end gap-3 px-6 py-4 border-t border-gray-200">
<button
type="button"
on:click={handleClose}
disabled={isSubmitting}
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border
border-gray-300 rounded-md shadow-sm hover:bg-gray-50
disabled:opacity-50"
>
Cancelar
</button>
<button
type="button"
on:click={handleSubmit}
disabled={isSubmitting || !content.trim()}
class="px-4 py-2 text-sm font-medium text-white bg-blue-700 border
border-transparent rounded-md shadow-sm hover:bg-blue-800
focus:outline-none focus:ring-2 focus:ring-offset-2
focus:ring-blue-500 disabled:opacity-50"
>
{#if isSubmitting}
<div class="flex items-center gap-2">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
<span>Creando...</span>
</div>
{:else}
Crear Asunto
{/if}
</button>
</div>
</div>
</div>
</div>

View File

@@ -5,8 +5,11 @@
import { api } from '$lib/utils/api'; import { api } from '$lib/utils/api';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ParticipantsModal from '$lib/components/ParticipantsModal.svelte'; import ParticipantsModal from '$lib/components/ParticipantsModal.svelte';
import IssueModal from '$lib/components/IssueModal.svelte';
let showParticipants = false; let showParticipants = false;
let showIssueModal = false;
let issues = [];
let ticketId: string; let ticketId: string;
let ticket = null; let ticket = null;
let comments = []; let comments = [];
@@ -56,6 +59,7 @@
comments = commentsData; comments = commentsData;
attachments = attachmentsData; attachments = attachmentsData;
users = usersData; users = usersData;
issues = issuesData;
} catch (e) { } catch (e) {
toast.error('Error cargando ticket: ' + (e.message || 'Error desconocido')); toast.error('Error cargando ticket: ' + (e.message || 'Error desconocido'));
} finally { } finally {
@@ -377,15 +381,46 @@
</div> </div>
<!-- Participantes --> <!-- Participantes -->
<div class="bg-white shadow rounded-lg"> <div class="bg-white shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200 flex justify-between items-center"> <div class="px-6 py-5 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900">Asunto</h3> <h3 class="text-lg font-semibold text-gray-900">
<button type="button" on:click={() => showParticipants = true} Asuntos {#if issues.length > 0}<span class="text-gray-400 font-normal">({issues.length})</span>{/if}
class="text-sm text-blue-600 hover:text-blue-700 font-medium"> </h3>
Gestionar <button
</button> type="button"
</div> on:click={() => showIssueModal = true}
class="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Crear asunto
</button>
</div>
{#if issues.length > 0}
<div class="divide-y divide-gray-100">
{#each issues as issue}
<div class="px-6 py-4">
<div class="flex items-start justify-between gap-2">
<p class="text-sm text-gray-700 flex-1">{issue.content}</p>
<span class="text-xs font-medium px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 shrink-0">
{issue.priority}
</span>
</div> </div>
{#if issue.tagged_users?.length > 0}
<div class="mt-2 flex flex-wrap gap-1">
{#each issue.tagged_users as u}
<span class="text-xs bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full">
{u.full_name}
</span>
{/each}
</div>
{/if}
<p class="text-xs text-gray-400 mt-1">{issue.created_by_name}</p>
</div>
{/each}
</div>
{:else}
<p class="px-6 py-4 text-sm text-gray-500">No hay asuntos creados.</p>
{/if}
</div>
<!-- SLA Information --> <!-- SLA Information -->
{#if ticket.sla_response_due || ticket.sla_resolution_due} {#if ticket.sla_response_due || ticket.sla_resolution_due}
@@ -449,4 +484,12 @@
on:close={() => showParticipants = false} on:close={() => showParticipants = false}
/> />
{/if} {/if}
{#if showIssueModal && ticket}
<IssueModal
ticketId={ticket.id}
{users}
on:close={() => showIssueModal = false}
on:created={() => { showIssueModal = false; loadData(); }}
/>
{/if}
</div> </div>