420 lines
16 KiB
TypeScript
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;
|