Adicionado modal de perfil do admin, corrigido emoji picker, tags em todas as caixas
Build and Deploy (Gitea) / build-and-deploy (push) Failing after 38s Details

This commit is contained in:
Sidney Gomes 2026-07-25 21:36:45 -03:00
parent 3b0313d5c0
commit 8dde8bf9e3
2 changed files with 100 additions and 39 deletions

View File

@ -3,7 +3,7 @@ import { useModal } from '../contexts/ModalContext';
import { import {
TrendingUp, Users, AlertTriangle, XCircle, Search, TrendingUp, Users, AlertTriangle, XCircle, Search,
Settings, CheckCircle2, RefreshCw, Link as LinkIcon, ShieldCheck, HelpCircle, Eye, DollarSign, Settings, CheckCircle2, RefreshCw, Link as LinkIcon, ShieldCheck, HelpCircle, Eye, DollarSign,
Shield, Layers, LogOut, Menu, X, GraduationCap, Mail, ThumbsUp, ThumbsDown Shield, Layers, LogOut, Menu, X, GraduationCap, Mail, ThumbsUp, ThumbsDown, UserCog
} from 'lucide-react'; } from 'lucide-react';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, Cell, PieChart, Pie } from 'recharts'; import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, Cell, PieChart, Pie } from 'recharts';
import { SubscriptionStatus } from '../types'; import { SubscriptionStatus } from '../types';
@ -11,6 +11,7 @@ import { AdminPricingManager } from './AdminPricingManager';
import { WhatsappConnectCard } from './WhatsappConnectCard'; import { WhatsappConnectCard } from './WhatsappConnectCard';
import AdminContentManager from './AdminContentManager'; import AdminContentManager from './AdminContentManager';
import AdminMessagesView from './AdminMessagesView'; import AdminMessagesView from './AdminMessagesView';
import AdminProfileModal from './AdminProfileModal';
import ToggleSwitch from './ToggleSwitch'; import ToggleSwitch from './ToggleSwitch';
interface WebhookLog { interface WebhookLog {
@ -92,6 +93,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
const [showSaveSuccessModal, setShowSaveSuccessModal] = useState(false); const [showSaveSuccessModal, setShowSaveSuccessModal] = useState(false);
const [asaasTestResult, setAsaasTestResult] = useState<{ loading: boolean; success: boolean | null; message: string } | null>(null); const [asaasTestResult, setAsaasTestResult] = useState<{ loading: boolean; success: boolean | null; message: string } | null>(null);
const [smtpTestResult, setSmtpTestResult] = useState<{ loading: boolean; success: boolean | null; message: string } | null>(null); const [smtpTestResult, setSmtpTestResult] = useState<{ loading: boolean; success: boolean | null; message: string } | null>(null);
const [showProfileModal, setShowProfileModal] = useState(false);
useEffect(() => { useEffect(() => {
fetchDashboardData(); fetchDashboardData();
@ -493,15 +495,20 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
{/* Footer profile card */} {/* Footer profile card */}
<div className="p-4 border-t border-white/5 space-y-4"> <div className="p-4 border-t border-white/5 space-y-4">
<div className="flex items-center space-x-3 bg-white/5 p-3 rounded-xl border border-white/5"> <button
<div className="w-9 h-9 rounded-full bg-[#E50914] text-white font-extrabold flex items-center justify-center text-xs shadow-md"> onClick={() => setShowProfileModal(true)}
className="w-full flex items-center space-x-3 bg-white/5 hover:bg-white/10 p-3 rounded-xl border border-white/5 hover:border-white/15 transition-all group text-left"
title="Editar Meu Perfil"
>
<div className="w-9 h-9 rounded-full bg-[#E50914] text-white font-extrabold flex items-center justify-center text-xs shadow-md flex-shrink-0">
{adminUser?.name ? adminUser.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase() : 'AD'} {adminUser?.name ? adminUser.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase() : 'AD'}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs font-bold text-white truncate">{adminUser?.name || 'Super Admin'}</p> <p className="text-xs font-bold text-white truncate">{adminUser?.name || 'Super Admin'}</p>
<p className="text-[10px] text-zinc-500 truncate">{adminUser?.email || 'admin@microtecflix.com'}</p> <p className="text-[10px] text-zinc-500 truncate">{adminUser?.email || 'admin@microtecflix.com'}</p>
</div> </div>
</div> <UserCog size={14} className="text-zinc-600 group-hover:text-zinc-300 transition-colors flex-shrink-0" />
</button>
{onLogout && ( {onLogout && (
<button <button
onClick={onLogout} onClick={onLogout}
@ -512,6 +519,13 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
</button> </button>
)} )}
</div> </div>
{/* Admin Profile Modal */}
<AdminProfileModal
isOpen={showProfileModal}
onClose={() => setShowProfileModal(false)}
token={token}
/>
</aside> </aside>
{/* BACKGROUND SHADOW FOR MOBILE SIDEBAR */} {/* BACKGROUND SHADOW FOR MOBILE SIDEBAR */}

View File

@ -275,8 +275,8 @@ export default function AdminMessagesView() {
const whatsappTemplates = [ const whatsappTemplates = [
{ id: 'whatsappTemplatePayment', title: 'Pagamento Confirmado', desc: 'Enviada automaticamente quando o aluno paga um plano ou curso.', icon: '💰', tags: ['{nome}', '{valor}', '{plano}', '{curso}'] }, { id: 'whatsappTemplatePayment', title: 'Pagamento Confirmado', desc: 'Enviada automaticamente quando o aluno paga um plano ou curso.', icon: '💰', tags: ['{nome}', '{valor}', '{plano}', '{curso}'] },
{ id: 'whatsappTemplateWelcome', title: 'Boas-Vindas (Nova Conta)', desc: 'Enviada automaticamente quando o aluno cria uma conta.', icon: '👋', tags: ['{nome}'] }, { id: 'whatsappTemplateWelcome', title: 'Boas-Vindas (Nova Conta)', desc: 'Enviada automaticamente quando o aluno cria uma conta.', icon: '👋', tags: ['{nome}', '{curso}', '{plano}', '{valor}'] },
{ id: 'whatsappTemplateNewContent', title: 'Novo Conteúdo (Carrossel)', desc: 'Enviada manualmente ao postar curso/módulo/aula.', icon: '🎬', tags: ['{nome}'] }, { id: 'whatsappTemplateNewContent', title: 'Novo Conteúdo (Carrossel)', desc: 'Enviada manualmente ao postar curso/módulo/aula.', icon: '🎬', tags: ['{nome}', '{curso}', '{modulo}', '{plano}', '{valor}'] },
]; ];
return ( return (
@ -531,27 +531,53 @@ export default function AdminMessagesView() {
ref={broadcastRef} ref={broadcastRef}
value={broadcastText} value={broadcastText}
onChange={(e) => setBroadcastText(e.target.value)} onChange={(e) => setBroadcastText(e.target.value)}
className="w-full h-40 bg-black/40 border border-white/10 rounded-lg p-3 pr-12 text-white text-sm focus:outline-none focus:border-amber-500/50 resize-none" className="w-full h-40 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-amber-500/50 resize-none"
placeholder="Escreva a mensagem de promoção aqui..." placeholder="Escreva a mensagem de promoção aqui..."
/> />
<div className="absolute bottom-3 right-3"> </div>
{/* Tags e emoji para broadcast */}
<div className="flex flex-wrap gap-2 items-center">
<span className="text-[11px] text-zinc-500">Tags:</span>
{['{nome}', '{curso}', '{modulo}', '{plano}', '{valor}'].map(tag => (
<code
key={tag}
className="bg-zinc-800 hover:bg-zinc-700 text-amber-300 px-1.5 py-0.5 rounded font-mono cursor-pointer border border-zinc-600 hover:border-amber-400 transition-colors text-[10px]"
title="Clique para inserir"
onClick={() => {
if (broadcastRef.current) {
const start = broadcastRef.current.selectionStart;
const end = broadcastRef.current.selectionEnd;
const newText = broadcastText.substring(0, start) + tag + broadcastText.substring(end);
setBroadcastText(newText);
setTimeout(() => {
broadcastRef.current?.focus();
broadcastRef.current?.setSelectionRange(start + tag.length, start + tag.length);
}, 0);
} else {
setBroadcastText(prev => prev + tag);
}
}}
>{tag}</code>
))}
<div className="relative ml-auto">
<button <button
onClick={() => setShowBroadcastEmojiPicker(!showBroadcastEmojiPicker)} onClick={() => setShowBroadcastEmojiPicker(!showBroadcastEmojiPicker)}
className="w-8 h-8 flex items-center justify-center text-zinc-400 hover:text-amber-400 transition-colors" className="flex items-center gap-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 hover:border-zinc-500 text-zinc-300 hover:text-white px-2.5 py-1 rounded-lg text-[11px] font-bold transition-all"
title="Inserir emoji"
> >
<Smile className="w-5 h-5" /> <Smile className="w-3.5 h-3.5" />
<span>Emoji</span>
</button> </button>
{showBroadcastEmojiPicker && (
<div className="absolute top-8 right-0 bg-zinc-900 border border-zinc-700 rounded-xl p-3 grid grid-cols-8 gap-1.5 z-50 shadow-2xl">
{EMOJI_LIST.map(emoji => (
<button key={emoji} onClick={() => insertEmoji(emoji, true)} className="text-xl hover:scale-125 transition-transform p-1 rounded hover:bg-white/10">
{emoji}
</button>
))}
</div>
)}
</div> </div>
{showBroadcastEmojiPicker && (
<div className="absolute bottom-12 right-3 bg-zinc-800 border border-white/10 rounded-xl p-3 grid grid-cols-6 gap-1.5 z-10 shadow-2xl">
{EMOJI_LIST.map(emoji => (
<button key={emoji} onClick={() => insertEmoji(emoji, true)} className="text-xl hover:scale-125 transition-transform p-1 rounded hover:bg-white/10">
{emoji}
</button>
))}
</div>
)}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -587,16 +613,34 @@ export default function AdminMessagesView() {
</div> </div>
)} )}
{/* Template Edit Modal */}
<CustomModal isOpen={!!activeTemplateModal} onClose={() => { setActiveTemplateModal(null); setShowEmojiPicker(false); }} title={whatsappTemplates.find(t => t.id === activeTemplateModal)?.title || 'Editar Template'} isGlass={true}> <CustomModal isOpen={!!activeTemplateModal} onClose={() => { setActiveTemplateModal(null); setShowEmojiPicker(false); }} title={whatsappTemplates.find(t => t.id === activeTemplateModal)?.title || 'Editar Template'} isGlass={true}>
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-blue-500/10 border border-blue-500/20 p-3 rounded-lg text-xs text-blue-200 space-y-1"> <div className="bg-blue-500/10 border border-blue-500/20 p-3 rounded-lg text-xs text-blue-200 space-y-2">
<p><strong>Formatação do WhatsApp:</strong> Use <code className="bg-black/40 px-1 rounded">*negrito*</code>, <code className="bg-black/40 px-1 rounded">_itálico_</code>, <code className="bg-black/40 px-1 rounded">~riscado~</code>.</p> <p><strong>Formatação do WhatsApp:</strong> Use <code className="bg-black/40 px-1 rounded">*negrito*</code>, <code className="bg-black/40 px-1 rounded">_itálico_</code>, <code className="bg-black/40 px-1 rounded">~riscado~</code>.</p>
<p><strong>Tags disponíveis:</strong>{' '} <div className="flex flex-wrap gap-1 items-center">
{whatsappTemplates.find(t => t.id === activeTemplateModal)?.tags.map(tag => ( <strong>Tags:</strong>
<code key={tag} className="bg-black/40 px-1 rounded font-bold text-blue-100 mr-1">{tag}</code> {['{nome}', '{curso}', '{modulo}', '{plano}', '{valor}'].map(tag => (
<code
key={tag}
className="bg-zinc-800 hover:bg-zinc-700 text-blue-300 px-1.5 py-0.5 rounded font-mono cursor-pointer border border-zinc-600 hover:border-blue-400 transition-colors"
title="Clique para inserir"
onClick={() => {
if (textareaRef.current) {
const start = textareaRef.current.selectionStart;
const end = textareaRef.current.selectionEnd;
const newText = currentTemplateText.substring(0, start) + tag + currentTemplateText.substring(end);
setCurrentTemplateText(newText);
setTimeout(() => {
textareaRef.current?.focus();
textareaRef.current?.setSelectionRange(start + tag.length, start + tag.length);
}, 0);
} else {
setCurrentTemplateText(prev => prev + tag);
}
}}
>{tag}</code>
))} ))}
</p> </div>
</div> </div>
<div className="relative"> <div className="relative">
@ -604,20 +648,23 @@ export default function AdminMessagesView() {
ref={textareaRef} ref={textareaRef}
value={currentTemplateText} value={currentTemplateText}
onChange={(e) => setCurrentTemplateText(e.target.value)} onChange={(e) => setCurrentTemplateText(e.target.value)}
className="w-full h-52 bg-black/40 border border-white/10 rounded-lg p-3 pr-12 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none" className="w-full h-52 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none"
placeholder="Escreva a mensagem aqui..." placeholder="Escreva a mensagem aqui..."
/> />
<div className="absolute bottom-3 right-3"> </div>
<button
onClick={() => setShowEmojiPicker(!showEmojiPicker)} {/* Emoji picker row - outside textarea, fully visible */}
className="w-8 h-8 flex items-center justify-center text-zinc-400 hover:text-[#E50914] transition-colors" <div className="relative">
title="Inserir emoji" <button
> onClick={() => setShowEmojiPicker(!showEmojiPicker)}
<Smile className="w-5 h-5" /> className="flex items-center gap-2 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 hover:border-zinc-500 text-zinc-300 hover:text-white px-3 py-1.5 rounded-lg text-xs font-bold transition-all"
</button> title="Inserir emoji"
</div> >
<Smile className="w-4 h-4" />
<span>Inserir Emoji</span>
</button>
{showEmojiPicker && ( {showEmojiPicker && (
<div className="absolute bottom-12 right-3 bg-zinc-800 border border-white/10 rounded-xl p-3 grid grid-cols-6 gap-1.5 z-10 shadow-2xl"> <div className="absolute top-10 left-0 bg-zinc-900 border border-zinc-700 rounded-xl p-3 grid grid-cols-8 gap-1.5 z-50 shadow-2xl">
{EMOJI_LIST.map(emoji => ( {EMOJI_LIST.map(emoji => (
<button key={emoji} onClick={() => insertEmoji(emoji, false)} className="text-xl hover:scale-125 transition-transform p-1 rounded hover:bg-white/10"> <button key={emoji} onClick={() => insertEmoji(emoji, false)} className="text-xl hover:scale-125 transition-transform p-1 rounded hover:bg-white/10">
{emoji} {emoji}