feat: suporte modal de tickets, aba de mensagens no admin, refatoracoes ui devflix e correcoes uppercase do brandName
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 1m4s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 1m4s
Details
This commit is contained in:
parent
8e19edb4f8
commit
be3e39e62d
|
|
@ -67,6 +67,7 @@ model User {
|
||||||
moduleGrades ModuleGrade[]
|
moduleGrades ModuleGrade[]
|
||||||
certificates Certificate[]
|
certificates Certificate[]
|
||||||
unlockedCourses String[] @default([]) @map("unlocked_courses")
|
unlockedCourses String[] @default([]) @map("unlocked_courses")
|
||||||
|
supportTickets SupportTicket[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
@ -281,3 +282,15 @@ model SystemSettings {
|
||||||
@@map("system_settings")
|
@@map("system_settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SupportTicket {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String @map("user_id")
|
||||||
|
content String @db.Text
|
||||||
|
status String @default("OPEN") // OPEN, REPLIED
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("support_tickets")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
111
server.ts
111
server.ts
|
|
@ -2211,16 +2211,123 @@ async function startServer() {
|
||||||
console.error('❌ Falha ao inicializar banco de dados:', dbError);
|
console.error('❌ Falha ao inicializar banco de dados:', dbError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
// --- Suporte / Mensagens ---
|
||||||
|
|
||||||
|
// Aluno envia um chamado de suporte/ajuda
|
||||||
|
app.post('/api/support-tickets', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { content } = req.body;
|
||||||
|
if (!content) return res.status(400).json({ error: 'Conteúdo é obrigatório' });
|
||||||
|
|
||||||
|
const ticket = await prisma.supportTicket.create({
|
||||||
|
data: {
|
||||||
|
userId: req.user!.id,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(ticket);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating support ticket:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin puxa todos os chamados e anotações
|
||||||
|
app.get('/api/admin/messages', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const tickets = await prisma.supportTicket.findMany({
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true, avatarUrl: true, whatsapp: true } }
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const notes = await prisma.note.findMany({
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, name: true, email: true, avatarUrl: true, whatsapp: true } },
|
||||||
|
lesson: { select: { title: true, module: { select: { course: { select: { title: true } } } } } }
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ tickets, notes });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching admin messages:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin responde um chamado ou anotação
|
||||||
|
app.post('/api/admin/reply-message', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { userId, message, ticketId } = req.body;
|
||||||
|
if (!userId || !message) return res.status(400).json({ error: 'userId e message são obrigatórios' });
|
||||||
|
|
||||||
|
// 1. Criar notificação no portal (Sininho)
|
||||||
|
await prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title: 'Nova mensagem de Suporte',
|
||||||
|
message,
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ticketId) {
|
||||||
|
await prisma.supportTicket.update({
|
||||||
|
where: { id: ticketId },
|
||||||
|
data: { status: 'REPLIED' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Enviar WhatsApp via Evolution API se configurado
|
||||||
|
const settings = await prisma.systemSettings.findUnique({ where: { id: 'default' } });
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
|
|
||||||
|
if (settings?.whatsappConnected && settings.evolutionApiUrl && settings.evolutionApiKey && user?.whatsapp) {
|
||||||
|
try {
|
||||||
|
const cleanPhone = user.whatsapp.replace(/\D/g, '');
|
||||||
|
if (cleanPhone.length >= 10) {
|
||||||
|
const numberId = `${cleanPhone}@s.whatsapp.net`;
|
||||||
|
const url = `${settings.evolutionApiUrl}/message/sendText/${settings.evolutionInstance}`;
|
||||||
|
|
||||||
|
await axios.post(url, {
|
||||||
|
number: numberId,
|
||||||
|
text: message,
|
||||||
|
options: { delay: 1200, linkPreview: true }
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'apikey': settings.evolutionApiKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`WhatsApp reply sent to ${cleanPhone}`);
|
||||||
|
}
|
||||||
|
} catch (waError: any) {
|
||||||
|
console.error('Failed to send WhatsApp reply:', waError?.response?.data || waError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error replying to message:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
const { createServer: createViteServer } = await import('vite');
|
const { createServer: createViteServer } = await import('vite');
|
||||||
const vite = await createViteServer({
|
const vite = await createViteServer({
|
||||||
server: { middlewareMode: true },
|
server: { middlewareMode: true },
|
||||||
appType: 'spa',
|
appType: 'spa',
|
||||||
});
|
});
|
||||||
app.use(vite.middlewares);
|
app.use(vite.middlewares);
|
||||||
} else {
|
} else {
|
||||||
const distPath = path.join(process.cwd(), 'dist');
|
const distPath = path.join(process.cwd(), 'dist');
|
||||||
app.use(express.static(distPath));
|
app.use(express.static(distPath));
|
||||||
|
|
||||||
|
// Fallback para SPA - Isso DEVE estar no final
|
||||||
app.get('*', (req: Request, res: Response) => {
|
app.get('*', (req: Request, res: Response) => {
|
||||||
res.sendFile(path.join(distPath, 'index.html'));
|
res.sendFile(path.join(distPath, 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import AdminDashboard from './components/AdminDashboard';
|
||||||
import AdminContentManager from './components/AdminContentManager';
|
import AdminContentManager from './components/AdminContentManager';
|
||||||
|
|
||||||
export default function AdminApp() {
|
export default function AdminApp() {
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('devflix_admin_token'));
|
const [token, setToken] = useState<string | null>(localStorage.getItem('admin_token'));
|
||||||
const [adminUser, setAdminUser] = useState<{
|
const [adminUser, setAdminUser] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -88,7 +88,7 @@ export default function AdminApp() {
|
||||||
throw new Error('Acesso recusado. Apenas administradores podem acessar esta área.');
|
throw new Error('Acesso recusado. Apenas administradores podem acessar esta área.');
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('devflix_admin_token', data.token);
|
localStorage.setItem('admin_token', data.token);
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setAdminUser(data.user);
|
setAdminUser(data.user);
|
||||||
setLoginEmail('');
|
setLoginEmail('');
|
||||||
|
|
@ -101,7 +101,7 @@ export default function AdminApp() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('devflix_admin_token');
|
localStorage.removeItem('admin_token');
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setAdminUser(null);
|
setAdminUser(null);
|
||||||
};
|
};
|
||||||
|
|
@ -131,7 +131,7 @@ export default function AdminApp() {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="font-display font-black text-2xl tracking-tighter text-white">
|
<h1 className="font-display font-black text-2xl tracking-tighter text-white">
|
||||||
{(settings?.brandName || 'MICROTEC').toUpperCase()}<span className="text-[#E50914]">FLIX</span> <span className="text-zinc-500 font-medium text-lg">ADMIN</span>
|
{(settings?.brandName || 'Plataforma')}<span className="text-[#E50914]">FLIX</span> <span className="text-zinc-500 font-medium text-lg">ADMIN</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[11px] text-zinc-400 font-medium">Autenticação obrigatória para gestores, administradores e moderadores.</p>
|
<p className="text-[11px] text-zinc-400 font-medium">Autenticação obrigatória para gestores, administradores e moderadores.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -156,7 +156,7 @@ export default function AdminApp() {
|
||||||
required
|
required
|
||||||
value={loginEmail}
|
value={loginEmail}
|
||||||
onChange={(e) => setLoginEmail(e.target.value)}
|
onChange={(e) => setLoginEmail(e.target.value)}
|
||||||
placeholder="email.admin@devflix.com"
|
placeholder="admin@email.com"
|
||||||
className="w-full glass-input rounded-xl p-3.5 pl-11 text-xs text-white focus:outline-none"
|
className="w-full glass-input rounded-xl p-3.5 pl-11 text-xs text-white focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
16
src/App.tsx
16
src/App.tsx
|
|
@ -15,7 +15,7 @@ import HelpView from './components/HelpView';
|
||||||
import { User as UserType, Course as CourseType, SubscriptionStatus } from './types';
|
import { User as UserType, Course as CourseType, SubscriptionStatus } from './types';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('devflix_token'));
|
const [token, setToken] = useState<string | null>(localStorage.getItem('app_token'));
|
||||||
const [user, setUser] = useState<{
|
const [user, setUser] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -66,7 +66,7 @@ export default function App() {
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setSettings(data);
|
setSettings(data);
|
||||||
if (data.brandName) {
|
if (data.brandName) {
|
||||||
document.title = `${data.brandName.charAt(0).toUpperCase() + data.brandName.slice(1)}Flix - Plataforma de Estudos`;
|
document.title = `${data.brandName}FLIX - Plataforma de Estudos`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Error fetching settings:', err));
|
.catch(err => console.error('Error fetching settings:', err));
|
||||||
|
|
@ -188,7 +188,7 @@ export default function App() {
|
||||||
throw new Error(data.error || 'Erro ao efetuar login');
|
throw new Error(data.error || 'Erro ao efetuar login');
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('devflix_token', data.token);
|
localStorage.setItem('app_token', data.token);
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
setLoginEmail('');
|
setLoginEmail('');
|
||||||
|
|
@ -218,7 +218,7 @@ export default function App() {
|
||||||
throw new Error(data.error || 'Erro ao efetuar cadastro');
|
throw new Error(data.error || 'Erro ao efetuar cadastro');
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('devflix_token', data.token);
|
localStorage.setItem('app_token', data.token);
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
setRegName('');
|
setRegName('');
|
||||||
|
|
@ -232,7 +232,7 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('devflix_token');
|
localStorage.removeItem('app_token');
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setView('home');
|
setView('home');
|
||||||
|
|
@ -456,12 +456,12 @@ export default function App() {
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t border-zinc-900 bg-zinc-950 py-8 px-4 text-center text-xs text-zinc-500 space-y-2 mt-12">
|
<footer className="border-t border-zinc-900 bg-zinc-950 py-8 px-4 text-center text-xs text-zinc-500 space-y-2 mt-12">
|
||||||
<p className="font-semibold text-zinc-400">{(settings?.brandName || 'MICROTEC').toUpperCase()}FLIX - Plataforma de Cursos Profissionalizantes</p>
|
<p className="font-semibold text-zinc-400">{(settings?.brandName || 'Plataforma')}FLIX - Plataforma de Cursos Profissionalizantes</p>
|
||||||
<p className="max-w-md mx-auto">Desenvolvido com foco em alta experiência de estudo para Windows, Pacote Office (Word, Excel, PowerPoint) e informática profissionalizante.</p>
|
<p className="max-w-md mx-auto">Desenvolvido com foco em alta experiência de estudo para Windows, Pacote Office (Word, Excel, PowerPoint) e informática profissionalizante.</p>
|
||||||
{settings?.cnpj && (
|
{settings?.cnpj && (
|
||||||
<p className="text-[11px] text-zinc-650">CNPJ: {settings.cnpj} • Endereço: {settings.address}</p>
|
<p className="text-[11px] text-zinc-650">CNPJ: {settings.cnpj} • Endereço: {settings.address}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-zinc-600">© 2026 {(settings?.brandName ? settings.brandName.charAt(0).toUpperCase() + settings.brandName.slice(1) : 'Microtec')}Flix Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.</p>
|
<p className="text-zinc-600">© 2026 {(settings?.brandName || 'Plataforma')}FLIX Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -478,7 +478,7 @@ export default function App() {
|
||||||
<Play className="fill-white w-6 h-6" />
|
<Play className="fill-white w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-display font-black text-2xl tracking-tighter text-[#E50914]">
|
<h1 className="font-display font-black text-2xl tracking-tighter text-[#E50914]">
|
||||||
{(settings?.brandName || 'MICROTEC').toUpperCase()}<span className="text-white">FLIX</span>
|
{(settings?.brandName || 'Plataforma')}<span className="text-white">FLIX</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-zinc-400 font-medium">
|
<p className="text-xs text-zinc-400 font-medium">
|
||||||
{settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.'}
|
{settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.'}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { SubscriptionStatus } from '../types';
|
||||||
import { AdminPricingManager } from './AdminPricingManager';
|
import { AdminPricingManager } from './AdminPricingManager';
|
||||||
import { WhatsappConnectCard } from './WhatsappConnectCard';
|
import { WhatsappConnectCard } from './WhatsappConnectCard';
|
||||||
import AdminContentManager from './AdminContentManager';
|
import AdminContentManager from './AdminContentManager';
|
||||||
|
import AdminMessagesView from './AdminMessagesView';
|
||||||
|
|
||||||
interface WebhookLog {
|
interface WebhookLog {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -75,7 +76,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
const [expandedStudentId, setExpandedStudentId] = useState<string | null>(null);
|
const [expandedStudentId, setExpandedStudentId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState<'dashboard' | 'students' | 'courses' | 'plans' | 'payments' | 'asaas_settings' | 'whatsapp_settings' | 'webhooks_logs' | 'saas_settings' | 'smtp_settings'>('dashboard');
|
const [activeTab, setActiveTab] = useState<'dashboard' | 'students' | 'courses' | 'plans' | 'payments' | 'messages' | 'asaas_settings' | 'whatsapp_settings' | 'webhooks_logs' | 'saas_settings' | 'smtp_settings'>('dashboard');
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||||
|
|
@ -357,7 +358,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
<Shield className="w-4 h-4" />
|
<Shield className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-display font-black text-base tracking-tighter text-[#E50914]">
|
<span className="font-display font-black text-base tracking-tighter text-[#E50914]">
|
||||||
{(settings?.brandName || 'MICROTEC').toUpperCase()}<span className="text-white">FLIX</span>
|
{(settings?.brandName || 'Plataforma')}<span className="text-white">FLIX</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -379,7 +380,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-display font-black text-lg md:text-xl leading-none tracking-tighter text-[#E50914]">
|
<span className="font-display font-black text-lg md:text-xl leading-none tracking-tighter text-[#E50914]">
|
||||||
{(settings?.brandName || 'MICROTEC').toUpperCase()}<span className="text-white">FLIX</span>
|
{(settings?.brandName || 'Plataforma')}<span className="text-white">FLIX</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-[#E50914] font-extrabold uppercase tracking-widest mt-0.5 leading-none">
|
<span className="text-[9px] text-[#E50914] font-extrabold uppercase tracking-widest mt-0.5 leading-none">
|
||||||
Admin Portal
|
Admin Portal
|
||||||
|
|
@ -433,6 +434,13 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
<DollarSign size={16} />
|
<DollarSign size={16} />
|
||||||
<span>Pagamentos</span>
|
<span>Pagamentos</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setActiveTab('messages'); setIsMobileMenuOpen(false); }}
|
||||||
|
className={`w-full flex items-center space-x-2.5 px-3 py-2 rounded-lg text-xs font-bold transition-all text-left ${activeTab === 'messages' ? 'bg-[#E50914]/10 text-white border-l-2 border-[#E50914]' : 'text-zinc-400 hover:text-white hover:bg-white/5'}`}
|
||||||
|
>
|
||||||
|
<Mail size={16} />
|
||||||
|
<span>Mensagens</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SISTEMA */}
|
{/* SISTEMA */}
|
||||||
|
|
@ -931,6 +939,11 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* TAB 5.5: MENSAGENS */}
|
||||||
|
{activeTab === 'messages' && (
|
||||||
|
<AdminMessagesView />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* TAB 6: ASAAS INTEGRATION CONFIG */}
|
{/* TAB 6: ASAAS INTEGRATION CONFIG */}
|
||||||
{activeTab === 'asaas_settings' && (
|
{activeTab === 'asaas_settings' && (
|
||||||
<div className="glass-card rounded-xl border border-white/10 p-6 shadow-2xl space-y-6">
|
<div className="glass-card rounded-xl border border-white/10 p-6 shadow-2xl space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Mail, MessageSquare, BookOpen, Send, User, Reply } from 'lucide-react';
|
||||||
|
|
||||||
|
interface SupportTicket {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
whatsapp: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Note {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
whatsapp: string | null;
|
||||||
|
};
|
||||||
|
lesson: {
|
||||||
|
title: string;
|
||||||
|
module: {
|
||||||
|
course: {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminMessagesView() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'tickets' | 'notes'>('tickets');
|
||||||
|
const [tickets, setTickets] = useState<SupportTicket[]>([]);
|
||||||
|
const [notes, setNotes] = useState<Note[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [replyText, setReplyText] = useState('');
|
||||||
|
const [replyingTo, setReplyingTo] = useState<string | null>(null);
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMessages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchMessages = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('admin_token');
|
||||||
|
const res = await fetch('/api/admin/messages', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setTickets(data.tickets);
|
||||||
|
setNotes(data.notes);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching messages:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReply = async (userId: string, ticketId?: string) => {
|
||||||
|
if (!replyText.trim()) return;
|
||||||
|
setIsSending(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('admin_token');
|
||||||
|
const res = await fetch('/api/admin/reply-message', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ userId, message: replyText, ticketId })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
setReplyText('');
|
||||||
|
setReplyingTo(null);
|
||||||
|
fetchMessages();
|
||||||
|
alert('Resposta enviada com sucesso!');
|
||||||
|
} else {
|
||||||
|
alert('Erro ao enviar resposta.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error replying:', error);
|
||||||
|
alert('Erro interno ao enviar resposta.');
|
||||||
|
} finally {
|
||||||
|
setIsSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white flex items-center space-x-2">
|
||||||
|
<Mail className="w-6 h-6 text-[#E50914]" />
|
||||||
|
<span>Mensagens e Comentários</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-zinc-400 text-sm mt-1">Gerencie dúvidas, anotações de aulas e chamados de suporte dos alunos.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-4 border-b border-white/10">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('tickets')}
|
||||||
|
className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'tickets' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
<span>Comentários / Ajuda ({tickets.length})</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('notes')}
|
||||||
|
className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'notes' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
|
||||||
|
>
|
||||||
|
<BookOpen className="w-4 h-4" />
|
||||||
|
<span>Anotações de Aulas ({notes.length})</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center py-20">
|
||||||
|
<div className="w-8 h-8 border-4 border-[#E50914] border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{activeTab === 'tickets' && (
|
||||||
|
tickets.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-zinc-500">Nenhum comentário recebido.</div>
|
||||||
|
) : (
|
||||||
|
tickets.map(ticket => (
|
||||||
|
<div key={ticket.id} className="bg-zinc-900 border border-white/10 rounded-xl p-5 space-y-4 relative">
|
||||||
|
{ticket.status === 'REPLIED' && (
|
||||||
|
<div className="absolute top-4 right-4 text-[10px] uppercase font-bold text-emerald-500 bg-emerald-500/10 px-2 py-1 rounded">
|
||||||
|
Respondido
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||||
|
{ticket.user.avatarUrl ? (
|
||||||
|
<img src={ticket.user.avatarUrl} alt={ticket.user.name} className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<User className="w-5 h-5 text-zinc-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-white text-sm">{ticket.user.name}</h4>
|
||||||
|
<p className="text-xs text-zinc-500">{ticket.user.email} {ticket.user.whatsapp && `• WA: ${ticket.user.whatsapp}`}</p>
|
||||||
|
<p className="text-[10px] text-zinc-600 mt-0.5">{new Date(ticket.createdAt).toLocaleString('pt-BR')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-black/20 p-4 rounded-lg text-zinc-300 text-sm whitespace-pre-wrap">
|
||||||
|
{ticket.content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{replyingTo === ticket.id ? (
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<textarea
|
||||||
|
value={replyText}
|
||||||
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
|
placeholder="Escreva sua resposta (será enviada via Notificação e WhatsApp se ativado)..."
|
||||||
|
className="w-full h-24 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { setReplyingTo(null); setReplyText(''); }}
|
||||||
|
className="px-3 py-1.5 text-xs text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleReply(ticket.user.id, ticket.id)}
|
||||||
|
disabled={!replyText.trim() || isSending}
|
||||||
|
className="bg-[#E50914] hover:bg-red-700 text-white px-4 py-1.5 rounded text-xs font-bold transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
{isSending ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-3 h-3" />
|
||||||
|
<span>Enviar Resposta</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => { setReplyingTo(ticket.id); setReplyText(''); }}
|
||||||
|
className="text-xs text-[#E50914] hover:text-red-400 transition-colors font-bold flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<Reply className="w-3 h-3" />
|
||||||
|
<span>Responder</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'notes' && (
|
||||||
|
notes.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-zinc-500">Nenhuma anotação registrada.</div>
|
||||||
|
) : (
|
||||||
|
notes.map(note => (
|
||||||
|
<div key={note.id} className="bg-zinc-900 border border-white/10 rounded-xl p-5 space-y-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||||
|
{note.user.avatarUrl ? (
|
||||||
|
<img src={note.user.avatarUrl} alt={note.user.name} className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<User className="w-5 h-5 text-zinc-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-white text-sm">{note.user.name}</h4>
|
||||||
|
<p className="text-xs text-zinc-500">{note.user.email}</p>
|
||||||
|
<p className="text-[10px] text-zinc-600 mt-0.5">{new Date(note.createdAt).toLocaleString('pt-BR')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right max-w-[200px]">
|
||||||
|
<span className="text-[10px] uppercase font-bold text-blue-500 bg-blue-500/10 px-2 py-1 rounded block truncate">
|
||||||
|
{note.lesson.module.course.title}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-zinc-400 block mt-1 truncate">{note.lesson.title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-black/20 p-4 rounded-lg text-zinc-300 text-sm whitespace-pre-wrap">
|
||||||
|
{note.content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{replyingTo === note.id ? (
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<textarea
|
||||||
|
value={replyText}
|
||||||
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
|
placeholder="Escreva sua resposta para esta anotação..."
|
||||||
|
className="w-full h-24 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { setReplyingTo(null); setReplyText(''); }}
|
||||||
|
className="px-3 py-1.5 text-xs text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleReply(note.user.id)}
|
||||||
|
disabled={!replyText.trim() || isSending}
|
||||||
|
className="bg-[#E50914] hover:bg-red-700 text-white px-4 py-1.5 rounded text-xs font-bold transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
{isSending ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-3 h-3" />
|
||||||
|
<span>Enviar Resposta</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => { setReplyingTo(note.id); setReplyText(''); }}
|
||||||
|
className="text-xs text-[#E50914] hover:text-red-400 transition-colors font-bold flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<Reply className="w-3 h-3" />
|
||||||
|
<span>Responder</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
|
||||||
const [showPlanModal, setShowPlanModal] = useState(false);
|
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||||
const [planForm, setPlanForm] = useState<Partial<Plan>>({ cycle: 'LIFETIME', active: true, includedCourses: [] });
|
const [planForm, setPlanForm] = useState<Partial<Plan>>({ cycle: 'LIFETIME', active: true, includedCourses: [] });
|
||||||
|
|
||||||
const token = localStorage.getItem('devflix_admin_token');
|
const token = localStorage.getItem('admin_token');
|
||||||
|
|
||||||
const fetchPlans = async () => {
|
const fetchPlans = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ArrowLeft, HelpCircle } from 'lucide-react';
|
import { ArrowLeft, HelpCircle, MessageSquare, X, Send } from 'lucide-react';
|
||||||
|
|
||||||
interface HelpViewProps {
|
interface HelpViewProps {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
|
@ -9,6 +9,10 @@ interface HelpViewProps {
|
||||||
export default function HelpView({ token, onBack }: HelpViewProps) {
|
export default function HelpView({ token, onBack }: HelpViewProps) {
|
||||||
const [helpText, setHelpText] = useState<string>('');
|
const [helpText, setHelpText] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [ticketContent, setTicketContent] = useState('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchHelpText();
|
fetchHelpText();
|
||||||
|
|
@ -31,6 +35,33 @@ export default function HelpView({ token, onBack }: HelpViewProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmitTicket = async () => {
|
||||||
|
if (!ticketContent.trim()) return;
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/support-tickets', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content: ticketContent })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
setTicketContent('');
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setSubmitSuccess(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating support ticket:', err);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl mx-auto">
|
<div className="space-y-6 max-w-4xl mx-auto">
|
||||||
<button
|
<button
|
||||||
|
|
@ -42,10 +73,19 @@ export default function HelpView({ token, onBack }: HelpViewProps) {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="glass-card rounded-2xl overflow-hidden shadow-2xl border border-white/10 p-8 min-h-[50vh]">
|
<div className="glass-card rounded-2xl overflow-hidden shadow-2xl border border-white/10 p-8 min-h-[50vh]">
|
||||||
<div className="flex items-center space-x-3 mb-8 border-b border-white/10 pb-4">
|
<div className="flex items-center justify-between mb-8 border-b border-white/10 pb-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
<HelpCircle className="w-8 h-8 text-[#E50914]" />
|
<HelpCircle className="w-8 h-8 text-[#E50914]" />
|
||||||
<h2 className="text-2xl font-bold text-white">Central de Ajuda</h2>
|
<h2 className="text-2xl font-bold text-white">Central de Ajuda</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
className="flex items-center space-x-2 bg-[#E50914] text-white px-4 py-2 rounded-lg font-bold hover:bg-red-700 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
<span>Falar com o Suporte</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center items-center py-12">
|
<div className="flex justify-center items-center py-12">
|
||||||
|
|
@ -57,6 +97,52 @@ export default function HelpView({ token, onBack }: HelpViewProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showModal && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
|
||||||
|
<div className="bg-zinc-900 border border-white/10 p-6 rounded-2xl w-full max-w-md">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-bold text-white">Enviar Comentário ou Dúvida</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
className="text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{submitSuccess ? (
|
||||||
|
<div className="bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 p-4 rounded-xl text-center">
|
||||||
|
<p className="font-bold">Mensagem enviada com sucesso!</p>
|
||||||
|
<p className="text-sm mt-1">Nossa equipe responderá em breve.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<textarea
|
||||||
|
value={ticketContent}
|
||||||
|
onChange={(e) => setTicketContent(e.target.value)}
|
||||||
|
placeholder="Escreva aqui sua dúvida, comentário ou solicitação de ajuda..."
|
||||||
|
className="w-full h-32 bg-zinc-800 border border-white/10 rounded-xl p-4 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmitTicket}
|
||||||
|
disabled={!ticketContent.trim() || isSubmitting}
|
||||||
|
className="w-full flex justify-center items-center space-x-2 bg-[#E50914] hover:bg-red-700 text-white font-bold py-3 px-4 rounded-xl transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Enviar Mensagem</span>
|
||||||
|
<Send className="w-4 h-4 ml-2" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ export default function Navbar({ user, activeView, setView, onLogout, settings }
|
||||||
<nav className="sticky top-0 z-50 glass-navbar px-4 py-3 md:px-8 flex items-center justify-between">
|
<nav className="sticky top-0 z-50 glass-navbar px-4 py-3 md:px-8 flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-8">
|
<div className="flex items-center space-x-8">
|
||||||
{/* LOGO */}
|
{/* LOGO */}
|
||||||
|
<div className="flex flex-col">
|
||||||
<div
|
<div
|
||||||
onClick={() => setView('home')}
|
onClick={() => setView('home')}
|
||||||
className="flex items-center space-x-2 cursor-pointer select-none group"
|
className="flex items-center space-x-2 cursor-pointer select-none group"
|
||||||
|
|
@ -111,9 +112,15 @@ export default function Navbar({ user, activeView, setView, onLogout, settings }
|
||||||
<Play className="fill-white w-5 h-5" />
|
<Play className="fill-white w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-sans font-black text-xl md:text-2xl tracking-tighter text-[#E50914]">
|
<span className="font-sans font-black text-xl md:text-2xl tracking-tighter text-[#E50914]">
|
||||||
{(settings?.brandName || 'MICROTEC').toUpperCase()}<span className="text-white">FLIX</span>
|
{(settings?.brandName || 'Plataforma')}<span className="text-white">FLIX</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{settings?.brandSlogan && (
|
||||||
|
<span className="text-[10px] text-zinc-400 font-medium ml-10 mt-0.5 max-w-[200px] truncate hidden md:block">
|
||||||
|
{settings.brandSlogan}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* NAVIGATION LINKS */}
|
{/* NAVIGATION LINKS */}
|
||||||
<div className="hidden md:flex items-center space-x-3 lg:space-x-6 text-sm flex-shrink-0">
|
<div className="hidden md:flex items-center space-x-3 lg:space-x-6 text-sm flex-shrink-0">
|
||||||
|
|
@ -226,7 +233,7 @@ export default function Navbar({ user, activeView, setView, onLogout, settings }
|
||||||
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<GraduationCap className="w-4 h-4" />
|
<GraduationCap className="w-4 h-4" />
|
||||||
<span>Meu Certificado</span>
|
<span>Meus Certificados</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('devflix_token');
|
const token = localStorage.getItem('app_token');
|
||||||
const res = await fetch('/api/users/profile', {
|
const res = await fetch('/api/users/profile', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -74,7 +74,7 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
|
||||||
formData.append('avatar', file);
|
formData.append('avatar', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('devflix_token');
|
const token = localStorage.getItem('app_token');
|
||||||
const res = await fetch('/api/users/avatar', {
|
const res = await fetch('/api/users/avatar', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ export default function SubscriptionView({ user, token, onStatusUpdate }: Subscr
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="font-display font-extrabold text-lg text-white tracking-tight">Assine o Plano Ilimitado</h3>
|
<h3 className="font-display font-extrabold text-lg text-white tracking-tight">Assine o Plano Ilimitado</h3>
|
||||||
<p className="text-xs text-zinc-500 font-semibold flex items-center space-x-1">
|
<p className="text-xs text-zinc-500 font-semibold flex items-center space-x-1">
|
||||||
<span>Trilha completa DevFlix por apenas</span>
|
<span>Trilha completa da Plataforma por apenas</span>
|
||||||
<span className="text-[#E50914] font-black text-sm">R$ 49,90/mês</span>
|
<span className="text-[#E50914] font-black text-sm">R$ 49,90/mês</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ export class AsaasService {
|
||||||
billingType: 'PIX' | 'CREDIT_CARD' | 'BOLETO',
|
billingType: 'PIX' | 'CREDIT_CARD' | 'BOLETO',
|
||||||
creditCardInfo?: any,
|
creditCardInfo?: any,
|
||||||
cycle: 'MONTHLY' | 'YEARLY' = 'MONTHLY',
|
cycle: 'MONTHLY' | 'YEARLY' = 'MONTHLY',
|
||||||
description: string = 'Assinatura Mensal DevFlix'
|
description: string = 'Assinatura Mensal da Plataforma'
|
||||||
): Promise<{ subscriptionId: string; payment: Partial<SubscriptionPayment> }> {
|
): Promise<{ subscriptionId: string; payment: Partial<SubscriptionPayment> }> {
|
||||||
const apiKey = await this.getApiKey();
|
const apiKey = await this.getApiKey();
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue