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;
|
||||
for (const userId of activeUsersIds) {
|
||||
const studentPayments = allPayments.filter(p => p.userId === userId && (p.status === 'CONFIRMED' || p.status === 'RECEIVED'));
|
||||
if (studentPayments.length > 0) {
|
||||
const latest = studentPayments[0];
|
||||
if (latest.planId && latest.plan) {
|
||||
if (latest.plan.cycle === 'MONTHLY') mrr += latest.plan.price;
|
||||
else if (latest.plan.cycle === 'YEARLY') mrr += (latest.plan.price / 12);
|
||||
} else if (!latest.courseId) {
|
||||
mrr += latest.value;
|
||||
// Find the latest payment that is a subscription (either a plan or no courseId)
|
||||
const latestSubscription = studentPayments.find(p => p.planId || (!p.courseId && !p.planId));
|
||||
|
||||
if (latestSubscription) {
|
||||
if (latestSubscription.planId && latestSubscription.plan) {
|
||||
if (latestSubscription.plan.cycle === 'MONTHLY') mrr += latestSubscription.plan.price;
|
||||
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" />
|
||||
<span>Meus dados</span>
|
||||
</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
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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