feat: Implement CRM and External Integrations
This commit is contained in:
419
src/pages/dashboard/crm/Campaigns.tsx
Normal file
419
src/pages/dashboard/crm/Campaigns.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user