microtecflix/src/components/AdminPricingManager.tsx

454 lines
21 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { RefreshCw, Save, Plus, Trash2, Edit, CheckCircle2, AlertTriangle, BookOpen, Layers, DollarSign } from 'lucide-react';
import { Course, Plan } from '../types';
interface AdminPricingManagerProps {
courses: Course[];
fetchCourses: () => void;
}
function CoursePricingCard({ course, saveLoading, onSave }: { course: Course, saveLoading: string | null, onSave: (id: string, locked: boolean, price: number) => void }) {
const [localPrice, setLocalPrice] = useState(course.price || 0);
const [localLocked, setLocalLocked] = useState(course.isLocked || false);
useEffect(() => {
setLocalPrice(course.price || 0);
setLocalLocked(course.isLocked || false);
}, [course.price, course.isLocked]);
const isChanged = localPrice !== (course.price || 0) || localLocked !== (course.isLocked || false);
return (
<div className="bg-zinc-900/50 border border-white/10 rounded-xl p-4 flex flex-col justify-between">
<div>
<h3 className="text-white font-bold mb-1 truncate" title={course.title}>{course.title}</h3>
<p className="text-xs text-zinc-500 mb-4">{course.category}</p>
<div className="flex items-center justify-between mb-4 bg-black/40 p-2 rounded-lg">
<span className="text-sm text-zinc-300 font-medium">Acesso</span>
<button
onClick={() => setLocalLocked(!localLocked)}
className={`px-3 py-1 rounded-md text-xs font-bold transition-all ${
localLocked ? 'bg-amber-500/20 text-amber-400 border border-amber-500/30' : 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
}`}
>
{localLocked ? '💰 Pago (Bloqueado)' : '🔓 Gratuito (Livre)'}
</button>
</div>
{localLocked && (
<div className="space-y-1 mb-4">
<label className="text-xs text-zinc-400 font-bold">Preço Avulso (R$)</label>
<div className="relative">
<DollarSign className="w-4 h-4 text-zinc-500 absolute left-3 top-3" />
<input
type="number"
step="0.01"
value={localPrice}
onChange={(e) => setLocalPrice(parseFloat(e.target.value) || 0)}
className="w-full bg-black/40 border border-white/10 rounded-lg pl-9 pr-3 py-2 text-sm text-white focus:outline-none focus:border-red-500 transition-colors"
/>
</div>
</div>
)}
</div>
<button
disabled={!isChanged || saveLoading === `course_${course.id}`}
onClick={() => onSave(course.id, localLocked, localPrice)}
className={`w-full py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all ${
isChanged
? 'bg-red-600 hover:bg-red-700 text-white shadow-[0_0_15px_rgba(220,38,38,0.3)]'
: 'bg-zinc-800 text-zinc-500 cursor-not-allowed'
}`}
>
{saveLoading === `course_${course.id}` ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Save className="w-4 h-4" />
)}
Salvar Alteração
</button>
</div>
);
}
export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ courses, fetchCourses }) => {
const [plans, setPlans] = useState<Plan[]>([]);
const [loading, setLoading] = useState(true);
const [saveLoading, setSaveLoading] = useState<string | null>(null);
// Modal state for Plans
const [showPlanModal, setShowPlanModal] = useState(false);
const [planForm, setPlanForm] = useState<Partial<Plan>>({ cycle: 'LIFETIME', active: true, includedCourses: [] });
const token = localStorage.getItem('devflix_token');
const fetchPlans = async () => {
setLoading(true);
try {
const res = await fetch('/api/admin/plans', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const data = await res.json();
setPlans(data);
}
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPlans();
}, []);
const handleUpdateCoursePrice = async (courseId: string, isLocked: boolean, price: number) => {
setSaveLoading(`course_${courseId}`);
try {
await fetch(`/api/admin/courses/${courseId}/price`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ isLocked, price })
});
fetchCourses();
} catch (err) {
console.error(err);
} finally {
setSaveLoading(null);
}
};
const handleSavePlan = async (e: React.FormEvent) => {
e.preventDefault();
setSaveLoading('plan_save');
try {
const url = planForm.id ? `/api/admin/plans/${planForm.id}` : '/api/admin/plans';
const method = planForm.id ? 'PUT' : 'POST';
await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(planForm)
});
await fetchPlans();
setShowPlanModal(false);
} catch (err) {
console.error(err);
} finally {
setSaveLoading(null);
}
};
const handleDeletePlan = async (planId: string) => {
if (!window.confirm('Tem certeza que deseja excluir este plano?')) return;
try {
await fetch(`/api/admin/plans/${planId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
fetchPlans();
} catch (err) {
console.error(err);
}
};
const toggleCourseInPlan = (courseId: string) => {
const current = planForm.includedCourses || [];
if (current.includes(courseId)) {
setPlanForm({ ...planForm, includedCourses: current.filter(id => id !== courseId) });
} else {
setPlanForm({ ...planForm, includedCourses: [...current, courseId] });
}
};
return (
<div className="space-y-8 animate-fade-in pb-12">
{/* SEÇÃO 1: Precificação Avulsa */}
<div className="bg-[#0a0a0a] rounded-xl border border-white/5 overflow-hidden shadow-2xl">
<div className="p-6 border-b border-white/5 flex justify-between items-center bg-gradient-to-r from-red-900/20 to-transparent">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<BookOpen className="w-5 h-5 text-red-500" />
Cursos Individuais (Venda Avulsa)
</h2>
<p className="text-zinc-400 text-sm mt-1">Configure quais cursos são pagos e defina o preço unitário vitalício.</p>
</div>
<button onClick={() => fetchCourses()} className="text-zinc-400 hover:text-white transition-colors">
<RefreshCw className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{courses.map(course => (
<CoursePricingCard
key={course.id}
course={course}
saveLoading={saveLoading}
onSave={handleUpdateCoursePrice}
/>
))}
</div>
</div>
</div>
{/* SEÇÃO 2: Planos e Combos */}
<div className="bg-[#0a0a0a] rounded-xl border border-white/5 overflow-hidden shadow-2xl">
<div className="p-6 border-b border-white/5 flex justify-between items-center bg-gradient-to-r from-purple-900/20 to-transparent">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-purple-500" />
Combos & Planos de Assinatura
</h2>
<p className="text-zinc-400 text-sm mt-1">Crie pacotes agrupando cursos ou assinaturas globais mensais/anuais.</p>
</div>
<button
onClick={() => { setPlanForm({ cycle: 'LIFETIME', active: true, includedCourses: [] }); setShowPlanModal(true); }}
className="bg-purple-600 hover:bg-purple-700 text-white text-sm font-bold py-2 px-4 rounded-lg flex items-center gap-2 transition-colors shadow-lg"
>
<Plus className="w-4 h-4" />
Criar Combo
</button>
</div>
<div className="p-6">
{loading ? (
<div className="flex justify-center py-12"><RefreshCw className="w-8 h-8 text-purple-500 animate-spin" /></div>
) : plans.length === 0 ? (
<div className="text-center py-12 border border-dashed border-white/10 rounded-xl bg-black/20">
<Layers className="w-12 h-12 text-zinc-700 mx-auto mb-3" />
<p className="text-zinc-400 font-medium">Nenhum plano ou combo criado.</p>
<p className="text-zinc-500 text-sm mt-1">Crie pacotes para oferecer descontos na compra de vários cursos juntos.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{plans.map(plan => (
<div key={plan.id} className={`border rounded-xl p-5 relative overflow-hidden flex flex-col ${
plan.active ? 'bg-zinc-900/40 border-purple-500/30' : 'bg-black/40 border-zinc-800 opacity-70'
}`}>
{!plan.active && (
<div className="absolute top-3 right-3 bg-red-500/20 text-red-400 text-xs font-bold px-2 py-1 rounded">Inativo</div>
)}
<div className="mb-4">
<div className="text-xs font-bold text-purple-400 mb-1 tracking-wider uppercase">
{plan.cycle === 'MONTHLY' ? 'Assinatura Mensal' : plan.cycle === 'YEARLY' ? 'Assinatura Anual' : 'Pagamento Único (Combo)'}
</div>
<h3 className="text-lg font-bold text-white mb-1">{plan.title}</h3>
<p className="text-sm text-zinc-400 line-clamp-2 min-h-[40px]">{plan.description}</p>
</div>
<div className="flex items-end gap-1 mb-6">
<span className="text-sm text-zinc-500 font-bold mb-1">R$</span>
<span className="text-3xl font-black text-white">{plan.price.toFixed(2).replace('.', ',')}</span>
{plan.cycle !== 'LIFETIME' && <span className="text-sm text-zinc-500 font-bold mb-1">/ {plan.cycle === 'MONTHLY' ? 'mês' : 'ano'}</span>}
</div>
<div className="bg-black/40 rounded-lg p-3 mb-6 flex-grow">
<span className="text-xs text-zinc-500 font-bold uppercase block mb-2">Cursos Inclusos:</span>
{plan.includedCourses.length === 0 ? (
<div className="flex items-center gap-2 text-emerald-400 text-sm font-medium">
<CheckCircle2 className="w-4 h-4" /> Acesso Global (Tudo)
</div>
) : (
<ul className="space-y-1">
{plan.includedCourses.slice(0, 3).map(cId => {
const c = courses.find(cx => cx.id === cId);
return c ? <li key={cId} className="text-sm text-zinc-300 truncate flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-purple-500" />{c.title}</li> : null;
})}
{plan.includedCourses.length > 3 && (
<li className="text-xs text-zinc-500 italic mt-1">+ {plan.includedCourses.length - 3} outros cursos</li>
)}
</ul>
)}
</div>
<div className="flex gap-2 mt-auto">
<button
onClick={() => { setPlanForm(plan); setShowPlanModal(true); }}
className="flex-1 bg-zinc-800 hover:bg-zinc-700 text-white text-sm font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-2"
>
<Edit className="w-4 h-4" /> Editar
</button>
<button
onClick={() => handleDeletePlan(plan.id)}
className="bg-red-950/30 hover:bg-red-900/50 text-red-500 text-sm font-bold p-2 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Plan Form Modal */}
{showPlanModal && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 overflow-y-auto">
<div className="bg-[#141414] border border-white/10 rounded-2xl w-full max-w-2xl overflow-hidden shadow-2xl my-8 relative">
<div className="p-6 border-b border-white/10 bg-gradient-to-r from-purple-900/20 to-transparent flex justify-between items-center">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-purple-500" />
{planForm.id ? 'Editar Plano / Combo' : 'Criar Novo Plano / Combo'}
</h2>
<button onClick={() => setShowPlanModal(false)} className="text-zinc-500 hover:text-white transition-colors">
</button>
</div>
<form onSubmit={handleSavePlan} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold">Nome do Plano/Combo</label>
<input
required
type="text"
value={planForm.title || ''}
onChange={e => setPlanForm({...planForm, title: e.target.value})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-purple-500 transition-colors"
placeholder="Ex: Combo Master Office"
/>
</div>
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold">Descrição Curta</label>
<textarea
value={planForm.description || ''}
onChange={e => setPlanForm({...planForm, description: e.target.value})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-purple-500 transition-colors resize-none h-20"
placeholder="Benefícios deste pacote..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold">Preço (R$)</label>
<input
required
type="number"
step="0.01"
value={planForm.price || ''}
onChange={e => setPlanForm({...planForm, price: parseFloat(e.target.value) || 0})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-purple-500 transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-xs text-zinc-400 font-bold">Ciclo Faturamento</label>
<select
value={planForm.cycle}
onChange={e => setPlanForm({...planForm, cycle: e.target.value as any})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-purple-500 transition-colors appearance-none"
>
<option value="LIFETIME">Pagamento Único (Vitalício)</option>
<option value="MONTHLY">Mensal (Assinatura)</option>
<option value="YEARLY">Anual (Assinatura)</option>
</select>
</div>
</div>
<label className="flex items-center gap-3 cursor-pointer p-3 bg-black/20 rounded-lg border border-white/5 hover:bg-black/40 transition-colors">
<input
type="checkbox"
checked={planForm.active}
onChange={e => setPlanForm({...planForm, active: e.target.checked})}
className="w-5 h-5 rounded border-zinc-600 text-purple-600 focus:ring-purple-600 focus:ring-offset-zinc-900 bg-zinc-800"
/>
<div>
<span className="text-sm font-bold text-white block">Plano Ativo (Visível)</span>
<span className="text-xs text-zinc-500">Alunos poderão comprar este plano.</span>
</div>
</label>
</div>
{/* Course Selection */}
<div className="bg-black/30 border border-white/5 rounded-xl flex flex-col max-h-[400px]">
<div className="p-4 border-b border-white/10 bg-zinc-900/50">
<h3 className="text-sm font-bold text-white flex items-center justify-between">
Cursos Inclusos no Pacote
<span className="text-xs bg-purple-500/20 text-purple-400 px-2 py-0.5 rounded-full">
{planForm.includedCourses?.length === 0 ? 'TUDO' : planForm.includedCourses?.length}
</span>
</h3>
</div>
<div className="p-4 overflow-y-auto space-y-2 flex-grow custom-scrollbar">
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
planForm.includedCourses?.length === 0
? 'bg-purple-900/20 border-purple-500/50'
: 'bg-zinc-900/50 border-white/5 hover:border-white/20'
}`}>
<input
type="checkbox"
checked={planForm.includedCourses?.length === 0}
onChange={() => setPlanForm({...planForm, includedCourses: []})}
className="w-5 h-5 rounded-full border-zinc-600 text-purple-600 focus:ring-purple-600 bg-zinc-800"
/>
<div>
<span className="text-sm font-bold text-white block">Acesso Global (All-Access)</span>
<span className="text-xs text-zinc-400">Libera todos os cursos, presentes e futuros.</span>
</div>
</label>
<div className="my-4 border-t border-white/10 relative">
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-black px-2 text-xs font-bold text-zinc-600">OU ESCOLHA ESPECÍFICOS</span>
</div>
{courses.map(course => (
<label key={course.id} className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
planForm.includedCourses?.includes(course.id)
? 'bg-purple-900/20 border-purple-500/50'
: 'bg-zinc-900/50 border-white/5 hover:border-white/20'
}`}>
<input
type="checkbox"
checked={planForm.includedCourses?.includes(course.id)}
onChange={() => toggleCourseInPlan(course.id)}
className="w-5 h-5 rounded border-zinc-600 text-purple-600 focus:ring-purple-600 bg-zinc-800"
/>
<span className="text-sm font-medium text-zinc-300 truncate">{course.title}</span>
</label>
))}
</div>
</div>
</div>
<div className="pt-6 border-t border-white/10 flex justify-end gap-3">
<button
type="button"
onClick={() => setShowPlanModal(false)}
className="px-6 py-2.5 rounded-lg text-sm font-bold text-zinc-300 hover:text-white hover:bg-white/5 transition-colors"
>
Cancelar
</button>
<button
type="submit"
disabled={saveLoading === 'plan_save'}
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2.5 rounded-lg text-sm font-bold shadow-lg shadow-purple-600/20 transition-all flex items-center gap-2"
>
{saveLoading === 'plan_save' ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
Salvar Plano
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};