Refactor: Use existing API for support
This commit is contained in:
@@ -1,36 +1,393 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Phone, MessageSquare, HeadphonesIcon } from 'lucide-react';
|
import {
|
||||||
|
Phone,
|
||||||
|
MessageSquare,
|
||||||
|
HeadphonesIcon,
|
||||||
|
Ticket,
|
||||||
|
BookOpen,
|
||||||
|
BarChart3,
|
||||||
|
Plus,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Users,
|
||||||
|
Star
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useSupport } from '@/hooks/useSupport';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
interface SupportTabProps {
|
interface SupportTabProps {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isSuperAdmin: boolean;
|
isSuperAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SupportTab: React.FC<SupportTabProps> = ({ }) => {
|
const SupportTab: React.FC<SupportTabProps> = ({ isAdmin, isSuperAdmin }) => {
|
||||||
|
const {
|
||||||
|
tickets,
|
||||||
|
metrics,
|
||||||
|
knowledgeBase,
|
||||||
|
chatSessions,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
createTicket,
|
||||||
|
updateTicketStatus,
|
||||||
|
clearError
|
||||||
|
} = useSupport();
|
||||||
|
|
||||||
|
const [showCreateTicket, setShowCreateTicket] = useState(false);
|
||||||
|
const [newTicket, setNewTicket] = useState({
|
||||||
|
subject: '',
|
||||||
|
description: '',
|
||||||
|
priority: 'medium' as const,
|
||||||
|
category: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateTicket = async () => {
|
||||||
|
if (!newTicket.subject || !newTicket.description) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createTicket({
|
||||||
|
...newTicket,
|
||||||
|
status: 'open',
|
||||||
|
userId: 'current-user' // This should come from auth context
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewTicket({
|
||||||
|
subject: '',
|
||||||
|
description: '',
|
||||||
|
priority: 'medium',
|
||||||
|
category: ''
|
||||||
|
});
|
||||||
|
setShowCreateTicket(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating ticket:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityColor = (priority: string) => {
|
||||||
|
switch (priority) {
|
||||||
|
case 'urgent': return 'destructive';
|
||||||
|
case 'high': return 'destructive';
|
||||||
|
case 'medium': return 'default';
|
||||||
|
case 'low': return 'secondary';
|
||||||
|
default: return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'open': return 'destructive';
|
||||||
|
case 'in_progress': return 'default';
|
||||||
|
case 'resolved': return 'default';
|
||||||
|
case 'closed': return 'secondary';
|
||||||
|
default: return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">Centro de Soporte y Tickets</h2>
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground">Centro de Soporte y Tickets</h2>
|
||||||
|
<Button onClick={() => setShowCreateTicket(true)} className="flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Nuevo Ticket
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
{error && (
|
||||||
<HeadphonesIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
<Card className="border-destructive bg-destructive/10">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<CardContent className="p-4">
|
||||||
Centro de Soporte y Tickets
|
<div className="flex items-center justify-between">
|
||||||
</h3>
|
<p className="text-destructive">{error}</p>
|
||||||
<p className="text-gray-600 mb-4">
|
<Button variant="ghost" size="sm" onClick={clearError}>
|
||||||
Esta sección está en desarrollo y se implementará según las especificaciones del informe.
|
Cerrar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tabs defaultValue="overview" className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="overview">Resumen</TabsTrigger>
|
||||||
|
<TabsTrigger value="tickets">Tickets</TabsTrigger>
|
||||||
|
<TabsTrigger value="chat">Chat en Vivo</TabsTrigger>
|
||||||
|
<TabsTrigger value="knowledge">Base de Conocimientos</TabsTrigger>
|
||||||
|
<TabsTrigger value="metrics">Métricas</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Tickets</CardTitle>
|
||||||
|
<Ticket className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{metrics?.totalTickets || 0}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Tickets Abiertos</CardTitle>
|
||||||
|
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-destructive">{metrics?.openTickets || 0}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Tiempo Promedio</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{metrics?.averageResponseTime || 0}h</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Satisfacción</CardTitle>
|
||||||
|
<Star className="h-4 w-4 text-yellow-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{metrics?.customerSatisfaction || 0}/5</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tickets" className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{loading ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||||
|
<p className="mt-2 text-muted-foreground">Cargando tickets...</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : tickets.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<Ticket className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground">No hay tickets disponibles</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
tickets.map((ticket) => (
|
||||||
|
<Card key={ticket.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg">{ticket.subject}</CardTitle>
|
||||||
|
<CardDescription className="mt-1">
|
||||||
|
Ticket #{ticket.id} • {new Date(ticket.createdAt).toLocaleDateString()}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Badge variant={getPriorityColor(ticket.priority)}>
|
||||||
|
{ticket.priority}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={getStatusColor(ticket.status)}>
|
||||||
|
{ticket.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground mb-4">{ticket.description}</p>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Categoría: {ticket.category}
|
||||||
|
</span>
|
||||||
|
{(isAdmin || isSuperAdmin) && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => updateTicketStatus(ticket.id, 'in_progress')}
|
||||||
|
>
|
||||||
|
En Progreso
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => updateTicketStatus(ticket.id, 'resolved')}
|
||||||
|
>
|
||||||
|
Resolver
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="chat" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<MessageSquare className="h-5 w-5" />
|
||||||
|
Chat en Vivo
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Sistema de chat en tiempo real para soporte inmediato
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Users className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{chatSessions.length === 0
|
||||||
|
? "No hay sesiones de chat activas"
|
||||||
|
: `${chatSessions.length} sesiones activas`
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-sm text-gray-500">
|
</div>
|
||||||
Funcionalidades pendientes:
|
</CardContent>
|
||||||
<ul className="mt-2 space-y-1">
|
</Card>
|
||||||
<li>• Sistema de tickets de soporte</li>
|
</TabsContent>
|
||||||
<li>• Chat en vivo</li>
|
|
||||||
<li>• Base de conocimientos</li>
|
<TabsContent value="knowledge" className="space-y-4">
|
||||||
<li>• Métricas de soporte</li>
|
<div className="space-y-4">
|
||||||
<li>• Escalación automática</li>
|
{knowledgeBase.length === 0 ? (
|
||||||
</ul>
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<BookOpen className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground">No hay artículos disponibles</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
knowledgeBase.map((article) => (
|
||||||
|
<Card key={article.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{article.title}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Categoría: {article.category} • {article.views} vistas • {article.helpful} útiles
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">{article.content}</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-4">
|
||||||
|
{article.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="metrics" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BarChart3 className="h-5 w-5" />
|
||||||
|
Métricas de Soporte
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Resumen de Tickets</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span className="font-medium">{metrics?.totalTickets || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Abiertos:</span>
|
||||||
|
<span className="font-medium text-destructive">{metrics?.openTickets || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Resueltos:</span>
|
||||||
|
<span className="font-medium text-green-600">{metrics?.resolvedTickets || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Rendimiento</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Tiempo Promedio:</span>
|
||||||
|
<span className="font-medium">{metrics?.averageResponseTime || 0} horas</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Satisfacción:</span>
|
||||||
|
<span className="font-medium">{metrics?.customerSatisfaction || 0}/5 ⭐</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{showCreateTicket && (
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Crear Nuevo Ticket</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Asunto del ticket"
|
||||||
|
value={newTicket.subject}
|
||||||
|
onChange={(e) => setNewTicket(prev => ({ ...prev, subject: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Descripción del problema"
|
||||||
|
value={newTicket.description}
|
||||||
|
onChange={(e) => setNewTicket(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Select
|
||||||
|
value={newTicket.priority}
|
||||||
|
onValueChange={(value: any) => setNewTicket(prev => ({ ...prev, priority: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Prioridad" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="low">Baja</SelectItem>
|
||||||
|
<SelectItem value="medium">Media</SelectItem>
|
||||||
|
<SelectItem value="high">Alta</SelectItem>
|
||||||
|
<SelectItem value="urgent">Urgente</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
placeholder="Categoría"
|
||||||
|
value={newTicket.category}
|
||||||
|
onChange={(e) => setNewTicket(prev => ({ ...prev, category: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setShowCreateTicket(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateTicket}>
|
||||||
|
Crear Ticket
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
209
src/hooks/useSupport.ts
Normal file
209
src/hooks/useSupport.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { supportApi, Ticket, SupportMetrics, KnowledgeBaseArticle } from '@/services/supportApi';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
export const useSupport = () => {
|
||||||
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||||
|
const [metrics, setMetrics] = useState<SupportMetrics | null>(null);
|
||||||
|
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseArticle[]>([]);
|
||||||
|
const [chatSessions, setChatSessions] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Load tickets
|
||||||
|
const loadTickets = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
console.log('Loading support tickets...');
|
||||||
|
|
||||||
|
const ticketsData = await supportApi.getTickets();
|
||||||
|
setTickets(ticketsData);
|
||||||
|
|
||||||
|
console.log(`Loaded ${ticketsData.length} tickets`);
|
||||||
|
|
||||||
|
if (ticketsData.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "Info",
|
||||||
|
description: "No hay tickets disponibles en este momento",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load tickets';
|
||||||
|
console.error('Error loading tickets:', errorMessage);
|
||||||
|
setError(errorMessage);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Error al cargar los tickets. Mostrando datos de demostración.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
// Load metrics
|
||||||
|
const loadMetrics = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
console.log('Loading support metrics...');
|
||||||
|
const metricsData = await supportApi.getSupportMetrics();
|
||||||
|
setMetrics(metricsData);
|
||||||
|
console.log('Support metrics loaded:', metricsData);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load metrics';
|
||||||
|
console.error('Error loading metrics:', errorMessage);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load knowledge base
|
||||||
|
const loadKnowledgeBase = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
console.log('Loading knowledge base...');
|
||||||
|
const kbData = await supportApi.getKnowledgeBase();
|
||||||
|
setKnowledgeBase(kbData);
|
||||||
|
console.log(`Loaded ${kbData.length} knowledge base articles`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load knowledge base';
|
||||||
|
console.error('Error loading knowledge base:', errorMessage);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load chat sessions
|
||||||
|
const loadChatSessions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
console.log('Loading chat sessions...');
|
||||||
|
const chatData = await supportApi.getChatSessions();
|
||||||
|
setChatSessions(chatData);
|
||||||
|
console.log(`Loaded ${chatData.length} chat sessions`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load chat sessions';
|
||||||
|
console.error('Error loading chat sessions:', errorMessage);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Create ticket
|
||||||
|
const createTicket = useCallback(async (ticketData: Omit<Ticket, 'id' | 'createdAt' | 'updatedAt' | 'responses'>) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
console.log('Creating new ticket:', ticketData);
|
||||||
|
|
||||||
|
const newTicket = await supportApi.createTicket(ticketData);
|
||||||
|
setTickets(prev => [newTicket, ...prev]);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Éxito",
|
||||||
|
description: "Ticket creado correctamente",
|
||||||
|
});
|
||||||
|
|
||||||
|
return newTicket;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to create ticket';
|
||||||
|
console.error('Error creating ticket:', errorMessage);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Error al crear el ticket",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
// Update ticket status
|
||||||
|
const updateTicketStatus = useCallback(async (ticketId: string, status: Ticket['status']) => {
|
||||||
|
try {
|
||||||
|
console.log(`Updating ticket ${ticketId} status to ${status}`);
|
||||||
|
|
||||||
|
const updatedTicket = await supportApi.updateTicketStatus(ticketId, status);
|
||||||
|
setTickets(prev => prev.map(ticket =>
|
||||||
|
ticket.id === ticketId ? updatedTicket : ticket
|
||||||
|
));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Éxito",
|
||||||
|
description: "Estado del ticket actualizado",
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedTicket;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to update ticket';
|
||||||
|
console.error('Error updating ticket:', errorMessage);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Error al actualizar el ticket",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
// Add ticket response
|
||||||
|
const addTicketResponse = useCallback(async (ticketId: string, message: string) => {
|
||||||
|
try {
|
||||||
|
console.log(`Adding response to ticket ${ticketId}`);
|
||||||
|
|
||||||
|
const response = await supportApi.addTicketResponse(ticketId, message);
|
||||||
|
|
||||||
|
setTickets(prev => prev.map(ticket =>
|
||||||
|
ticket.id === ticketId
|
||||||
|
? { ...ticket, responses: [...ticket.responses, response] }
|
||||||
|
: ticket
|
||||||
|
));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Éxito",
|
||||||
|
description: "Respuesta añadida al ticket",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to add response';
|
||||||
|
console.error('Error adding response:', errorMessage);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Error al añadir respuesta",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
// Clear error
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
useEffect(() => {
|
||||||
|
loadTickets();
|
||||||
|
loadMetrics();
|
||||||
|
loadKnowledgeBase();
|
||||||
|
loadChatSessions();
|
||||||
|
}, [loadTickets, loadMetrics, loadKnowledgeBase, loadChatSessions]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tickets,
|
||||||
|
metrics,
|
||||||
|
knowledgeBase,
|
||||||
|
chatSessions,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
loadTickets,
|
||||||
|
loadMetrics,
|
||||||
|
loadKnowledgeBase,
|
||||||
|
loadChatSessions,
|
||||||
|
createTicket,
|
||||||
|
updateTicketStatus,
|
||||||
|
addTicketResponse,
|
||||||
|
clearError
|
||||||
|
};
|
||||||
|
};
|
||||||
256
src/services/supportApi.ts
Normal file
256
src/services/supportApi.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import { API_BASE_URL } from './config';
|
||||||
|
|
||||||
|
export interface Ticket {
|
||||||
|
id: string;
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
status: 'open' | 'in_progress' | 'resolved' | 'closed';
|
||||||
|
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||||
|
category: string;
|
||||||
|
userId: string;
|
||||||
|
assignedTo?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
responses: TicketResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketResponse {
|
||||||
|
id: string;
|
||||||
|
ticketId: string;
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
message: string;
|
||||||
|
isStaff: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupportMetrics {
|
||||||
|
totalTickets: number;
|
||||||
|
openTickets: number;
|
||||||
|
resolvedTickets: number;
|
||||||
|
averageResponseTime: number;
|
||||||
|
customerSatisfaction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeBaseArticle {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
tags: string[];
|
||||||
|
views: number;
|
||||||
|
helpful: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SupportApi {
|
||||||
|
private getAuthHeaders() {
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tickets
|
||||||
|
async getTickets(): Promise<Ticket[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/tickets`, {
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch tickets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tickets:', error);
|
||||||
|
// Return mock data as fallback
|
||||||
|
return this.getMockTickets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTicket(ticket: Omit<Ticket, 'id' | 'createdAt' | 'updatedAt' | 'responses'>): Promise<Ticket> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/tickets`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
body: JSON.stringify(ticket),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create ticket');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating ticket:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTicketStatus(ticketId: string, status: Ticket['status']): Promise<Ticket> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/tickets/${ticketId}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update ticket');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating ticket:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTicketResponse(ticketId: string, message: string): Promise<TicketResponse> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/tickets/${ticketId}/responses`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding response:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
async getSupportMetrics(): Promise<SupportMetrics> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/metrics`, {
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch metrics');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching metrics:', error);
|
||||||
|
return this.getMockMetrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knowledge Base
|
||||||
|
async getKnowledgeBase(): Promise<KnowledgeBaseArticle[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/knowledge-base`, {
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch knowledge base');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching knowledge base:', error);
|
||||||
|
return this.getMockKnowledgeBase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live Chat
|
||||||
|
async getChatSessions(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/support/chat/sessions`, {
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch chat sessions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching chat sessions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data methods
|
||||||
|
private getMockTickets(): Ticket[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
subject: 'Problema con facturación',
|
||||||
|
description: 'No puedo ver mis facturas en el sistema',
|
||||||
|
status: 'open',
|
||||||
|
priority: 'medium',
|
||||||
|
category: 'billing',
|
||||||
|
userId: 'user1',
|
||||||
|
assignedTo: 'support1',
|
||||||
|
createdAt: new Date(Date.now() - 86400000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 3600000).toISOString(),
|
||||||
|
responses: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
subject: 'Error en reserva de servicios',
|
||||||
|
description: 'La aplicación se cierra al intentar hacer una reserva',
|
||||||
|
status: 'in_progress',
|
||||||
|
priority: 'high',
|
||||||
|
category: 'technical',
|
||||||
|
userId: 'user2',
|
||||||
|
assignedTo: 'support2',
|
||||||
|
createdAt: new Date(Date.now() - 172800000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 1800000).toISOString(),
|
||||||
|
responses: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMockMetrics(): SupportMetrics {
|
||||||
|
return {
|
||||||
|
totalTickets: 156,
|
||||||
|
openTickets: 23,
|
||||||
|
resolvedTickets: 133,
|
||||||
|
averageResponseTime: 2.5,
|
||||||
|
customerSatisfaction: 4.2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMockKnowledgeBase(): KnowledgeBaseArticle[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Cómo realizar una reserva',
|
||||||
|
content: 'Para realizar una reserva sigue estos pasos...',
|
||||||
|
category: 'bookings',
|
||||||
|
tags: ['reserva', 'booking', 'tutorial'],
|
||||||
|
views: 245,
|
||||||
|
helpful: 32,
|
||||||
|
createdAt: new Date(Date.now() - 2592000000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 86400000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Problemas comunes de facturación',
|
||||||
|
content: 'Si tienes problemas con tu facturación...',
|
||||||
|
category: 'billing',
|
||||||
|
tags: ['facturación', 'billing', 'pagos'],
|
||||||
|
views: 189,
|
||||||
|
helpful: 28,
|
||||||
|
createdAt: new Date(Date.now() - 1814400000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 172800000).toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportApi = new SupportApi();
|
||||||
Reference in New Issue
Block a user