Asuntos Internal Implementado
This commit is contained in:
230
frontend-internal/src/lib/components/IssueModal.svelte
Normal file
230
frontend-internal/src/lib/components/IssueModal.svelte
Normal 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>
|
||||
@@ -5,8 +5,11 @@
|
||||
import { api } from '$lib/utils/api';
|
||||
import { onMount } from 'svelte';
|
||||
import ParticipantsModal from '$lib/components/ParticipantsModal.svelte';
|
||||
import IssueModal from '$lib/components/IssueModal.svelte';
|
||||
|
||||
let showParticipants = false;
|
||||
let showIssueModal = false;
|
||||
let issues = [];
|
||||
let ticketId: string;
|
||||
let ticket = null;
|
||||
let comments = [];
|
||||
@@ -56,6 +59,7 @@
|
||||
comments = commentsData;
|
||||
attachments = attachmentsData;
|
||||
users = usersData;
|
||||
issues = issuesData;
|
||||
} catch (e) {
|
||||
toast.error('Error cargando ticket: ' + (e.message || 'Error desconocido'));
|
||||
} finally {
|
||||
@@ -379,13 +383,44 @@
|
||||
<!-- Participantes -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<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>
|
||||
<button type="button" on:click={() => showParticipants = true}
|
||||
class="text-sm text-blue-600 hover:text-blue-700 font-medium">
|
||||
Gestionar
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
Asuntos {#if issues.length > 0}<span class="text-gray-400 font-normal">({issues.length})</span>{/if}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
{#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 -->
|
||||
{#if ticket.sla_response_due || ticket.sla_resolution_due}
|
||||
@@ -449,4 +484,12 @@
|
||||
on:close={() => showParticipants = false}
|
||||
/>
|
||||
{/if}
|
||||
{#if showIssueModal && ticket}
|
||||
<IssueModal
|
||||
ticketId={ticket.id}
|
||||
{users}
|
||||
on:close={() => showIssueModal = false}
|
||||
on:created={() => { showIssueModal = false; loadData(); }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user