Files
karibeo_backend_admin/src/pages/dashboard/crm/Campaigns.tsx
2025-10-11 16:13:55 +00:00

420 lines
16 KiB
TypeScript

import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Mail,
Plus,
Send,
Clock,
CheckCircle,
XCircle,
BarChart3,
Users,
Eye,
MousePointer,
ShoppingCart,
Calendar
} from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { z } from 'zod';
const campaignSchema = z.object({
name: z.string().trim().min(1, 'Nombre requerido').max(100, 'Nombre muy largo'),
subject: z.string().trim().min(1, 'Asunto requerido').max(200, 'Asunto muy largo'),
segment: z.string().min(1, 'Selecciona un segmento'),
message: z.string().trim().min(10, 'Mensaje muy corto').max(2000, 'Mensaje muy largo'),
});
const Campaigns = () => {
const { toast } = useToast();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [formData, setFormData] = useState({
name: '',
subject: '',
segment: '',
message: ''
});
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const campaigns = [
{
id: 1,
name: 'Promoción Verano 2024',
status: 'sent',
segment: 'VIP Travelers',
sentDate: '2024-03-10',
recipients: 2847,
opened: 1423,
clicked: 456,
conversions: 89,
revenue: '$12,450'
},
{
id: 2,
name: 'Newsletter Mensual',
status: 'scheduled',
segment: 'Todos',
sentDate: '2024-03-20',
recipients: 5234,
opened: 0,
clicked: 0,
conversions: 0,
revenue: '$0'
},
{
id: 3,
name: 'Reactivación Clientes',
status: 'sending',
segment: 'Inactivos',
sentDate: '2024-03-15',
recipients: 1245,
opened: 623,
clicked: 187,
conversions: 34,
revenue: '$4,230'
},
{
id: 4,
name: 'Ofertas Especiales Familias',
status: 'draft',
segment: 'Familias',
sentDate: null,
recipients: 891,
opened: 0,
clicked: 0,
conversions: 0,
revenue: '$0'
}
];
const segments = ['Todos', 'VIP Travelers', 'Business', 'Familias', 'Aventureros', 'Inactivos'];
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
try {
campaignSchema.parse(formData);
setFormErrors({});
toast({
title: 'Campaña creada',
description: `La campaña "${formData.name}" ha sido creada exitosamente.`,
});
setIsCreateDialogOpen(false);
setFormData({ name: '', subject: '', segment: '', message: '' });
} catch (error) {
if (error instanceof z.ZodError) {
const errors: Record<string, string> = {};
error.errors.forEach((err) => {
if (err.path[0]) {
errors[err.path[0].toString()] = err.message;
}
});
setFormErrors(errors);
}
}
};
const getStatusBadge = (status: string) => {
const statusConfig = {
sent: { label: 'Enviada', variant: 'default' as const, icon: CheckCircle, color: 'text-green-600' },
scheduled: { label: 'Programada', variant: 'secondary' as const, icon: Clock, color: 'text-blue-600' },
sending: { label: 'Enviando', variant: 'outline' as const, icon: Send, color: 'text-orange-600' },
draft: { label: 'Borrador', variant: 'outline' as const, icon: Mail, color: 'text-gray-600' },
failed: { label: 'Fallida', variant: 'destructive' as const, icon: XCircle, color: 'text-red-600' }
};
return statusConfig[status as keyof typeof statusConfig] || statusConfig.draft;
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Campañas</h1>
<p className="text-gray-600 mt-1">Gestiona tus campañas de marketing</p>
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Nueva Campaña
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Crear Nueva Campaña</DialogTitle>
<DialogDescription>
Configura tu campaña de email marketing
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">Nombre de la Campaña *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Ej: Promoción Verano 2024"
className={formErrors.name ? 'border-red-500' : ''}
/>
{formErrors.name && (
<p className="text-sm text-red-500 mt-1">{formErrors.name}</p>
)}
</div>
<div>
<Label htmlFor="subject">Asunto del Email *</Label>
<Input
id="subject"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
placeholder="Asunto atractivo para tus clientes"
className={formErrors.subject ? 'border-red-500' : ''}
/>
{formErrors.subject && (
<p className="text-sm text-red-500 mt-1">{formErrors.subject}</p>
)}
</div>
<div>
<Label htmlFor="segment">Segmento Objetivo *</Label>
<Select
value={formData.segment}
onValueChange={(value) => setFormData({ ...formData, segment: value })}
>
<SelectTrigger className={formErrors.segment ? 'border-red-500' : ''}>
<SelectValue placeholder="Selecciona el público objetivo" />
</SelectTrigger>
<SelectContent>
{segments.map((segment) => (
<SelectItem key={segment} value={segment}>
{segment}
</SelectItem>
))}
</SelectContent>
</Select>
{formErrors.segment && (
<p className="text-sm text-red-500 mt-1">{formErrors.segment}</p>
)}
</div>
<div>
<Label htmlFor="message">Mensaje *</Label>
<Textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
placeholder="Escribe el contenido de tu email..."
rows={6}
className={formErrors.message ? 'border-red-500' : ''}
/>
{formErrors.message && (
<p className="text-sm text-red-500 mt-1">{formErrors.message}</p>
)}
<p className="text-sm text-gray-500 mt-1">
{formData.message.length}/2000 caracteres
</p>
</div>
<div className="flex justify-end gap-3 mt-6">
<Button type="button" variant="outline" onClick={() => setIsCreateDialogOpen(false)}>
Cancelar
</Button>
<Button type="button" variant="outline">
Guardar Borrador
</Button>
<Button type="submit">
Crear Campaña
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">
Total Enviados
</CardTitle>
<Mail className="h-5 w-5 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">9,326</div>
<p className="text-xs text-gray-500 mt-1">Este mes</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">
Tasa de Apertura
</CardTitle>
<Eye className="h-5 w-5 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">47.3%</div>
<p className="text-xs text-gray-500 mt-1">Promedio</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">
Click Rate
</CardTitle>
<MousePointer className="h-5 w-5 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">18.6%</div>
<p className="text-xs text-gray-500 mt-1">Promedio</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">
Ingresos Generados
</CardTitle>
<ShoppingCart className="h-5 w-5 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">$16,680</div>
<p className="text-xs text-gray-500 mt-1">Este mes</p>
</CardContent>
</Card>
</div>
{/* Campaigns List */}
<Card>
<CardHeader>
<CardTitle>Lista de Campañas</CardTitle>
<CardDescription>Todas tus campañas de email marketing</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{campaigns.map((campaign) => {
const statusConfig = getStatusBadge(campaign.status);
const StatusIcon = statusConfig.icon;
return (
<div key={campaign.id} className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg bg-gradient-to-r from-orange-500 to-red-500 flex items-center justify-center`}>
<Mail className="h-5 w-5 text-white" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-gray-900">{campaign.name}</h3>
<Badge variant={statusConfig.variant} className="flex items-center gap-1">
<StatusIcon className={`h-3 w-3 ${statusConfig.color}`} />
{statusConfig.label}
</Badge>
</div>
<div className="flex items-center gap-4 mt-1 text-sm text-gray-600">
<div className="flex items-center gap-1">
<Users className="h-4 w-4" />
<span>{campaign.segment}</span>
</div>
{campaign.sentDate && (
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>{campaign.sentDate}</span>
</div>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<BarChart3 className="h-4 w-4 mr-2" />
Ver Resultados
</Button>
{campaign.status === 'draft' && (
<Button size="sm">
<Send className="h-4 w-4 mr-2" />
Enviar
</Button>
)}
</div>
</div>
{campaign.status !== 'draft' && (
<div className="grid grid-cols-5 gap-4 pt-4 border-t">
<div>
<p className="text-sm text-gray-600">Destinatarios</p>
<p className="text-lg font-semibold text-gray-900">
{campaign.recipients.toLocaleString()}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Abiertos</p>
<p className="text-lg font-semibold text-gray-900">
{campaign.opened.toLocaleString()}
{campaign.recipients > 0 && (
<span className="text-sm text-gray-500 ml-1">
({((campaign.opened / campaign.recipients) * 100).toFixed(1)}%)
</span>
)}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Clicks</p>
<p className="text-lg font-semibold text-gray-900">
{campaign.clicked.toLocaleString()}
{campaign.opened > 0 && (
<span className="text-sm text-gray-500 ml-1">
({((campaign.clicked / campaign.opened) * 100).toFixed(1)}%)
</span>
)}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Conversiones</p>
<p className="text-lg font-semibold text-gray-900">
{campaign.conversions.toLocaleString()}
{campaign.recipients > 0 && (
<span className="text-sm text-gray-500 ml-1">
({((campaign.conversions / campaign.recipients) * 100).toFixed(1)}%)
</span>
)}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Ingresos</p>
<p className="text-lg font-semibold text-green-600">
{campaign.revenue}
</p>
</div>
</div>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
);
};
export default Campaigns;