fix(dashboard): MRR calculation and user menu
This commit is contained in:
parent
8236ade11a
commit
cc9b906eeb
|
|
@ -803,13 +803,15 @@ app.get('/api/admin/dashboard', authenticateToken, requireAdmin, async (req: Aut
|
||||||
let mrr = 0;
|
let mrr = 0;
|
||||||
for (const userId of activeUsersIds) {
|
for (const userId of activeUsersIds) {
|
||||||
const studentPayments = allPayments.filter(p => p.userId === userId && (p.status === 'CONFIRMED' || p.status === 'RECEIVED'));
|
const studentPayments = allPayments.filter(p => p.userId === userId && (p.status === 'CONFIRMED' || p.status === 'RECEIVED'));
|
||||||
if (studentPayments.length > 0) {
|
// Find the latest payment that is a subscription (either a plan or no courseId)
|
||||||
const latest = studentPayments[0];
|
const latestSubscription = studentPayments.find(p => p.planId || (!p.courseId && !p.planId));
|
||||||
if (latest.planId && latest.plan) {
|
|
||||||
if (latest.plan.cycle === 'MONTHLY') mrr += latest.plan.price;
|
if (latestSubscription) {
|
||||||
else if (latest.plan.cycle === 'YEARLY') mrr += (latest.plan.price / 12);
|
if (latestSubscription.planId && latestSubscription.plan) {
|
||||||
} else if (!latest.courseId) {
|
if (latestSubscription.plan.cycle === 'MONTHLY') mrr += latestSubscription.plan.price;
|
||||||
mrr += latest.value;
|
else if (latestSubscription.plan.cycle === 'YEARLY') mrr += (latestSubscription.plan.price / 12);
|
||||||
|
} else if (!latestSubscription.courseId) {
|
||||||
|
mrr += latestSubscription.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,13 @@ export default function Navbar({ user, activeView, setView, onLogout }: NavbarPr
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
<span>Meus dados</span>
|
<span>Meus dados</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setView('certificates'); setShowProfileMenu(false); }}
|
||||||
|
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" />
|
||||||
|
<span>Meu Certificado</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
// Mock types since we will split these out
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
user?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
const s3 = new S3Client({
|
||||||
|
region: 'us-east-1',
|
||||||
|
endpoint: 'https://s3-estudo.microtecinformaticacurso.com.br',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: 'microtecflix_admin',
|
||||||
|
secretAccessKey: 'MicrotecFlixS3SecurePass2026!'
|
||||||
|
},
|
||||||
|
forcePathStyle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const authenticateToken = (req: any, res: any, next: any) => {
|
||||||
|
// Logic will be in server.ts or a middleware file, assuming it's passed down
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
router.put('/profile', async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const { name, whatsapp, birthDate, cpf } = req.body;
|
||||||
|
const userId = req.user!.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
...(name !== undefined && { name }),
|
||||||
|
...(whatsapp !== undefined && { whatsapp }),
|
||||||
|
...(birthDate !== undefined && { birthDate }),
|
||||||
|
...(cpf !== undefined && { cpf })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.json({ success: true, user });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({ error: 'Usuário não encontrado' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/avatar', upload.single('avatar'), async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const userId = req.user!.id;
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = req.file.originalname.split('.').pop();
|
||||||
|
const filename = `avatars/${userId}_${Date.now()}.${ext}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3.send(new PutObjectCommand({
|
||||||
|
Bucket: 'microtecflix',
|
||||||
|
Key: filename,
|
||||||
|
Body: req.file.buffer,
|
||||||
|
ContentType: req.file.mimetype
|
||||||
|
}));
|
||||||
|
|
||||||
|
const avatarUrl = `https://s3-estudo.microtecinformaticacurso.com.br/microtecflix/${filename}`;
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { avatarUrl }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, avatarUrl });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Erro no upload da foto:', err);
|
||||||
|
res.status(500).json({ error: 'Erro ao salvar foto no MinIO' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
Loading…
Reference in New Issue