mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-23 23:28:03 +02:00
Ajout de l'application Presencia (gestion des présences multi-sociétés)
Application complète : API Node/Express + PostgreSQL, frontend HTML/JS mono-page, et déploiement Docker sur ports non standards (8781/4790/6543). - Cadres : calendrier mensuel, saisie de présence par demi-journée (présent/absent/congé/RTT), validation de fin de mois qui verrouille leurs propres saisies. - Admin : gestion des sociétés et utilisateurs (rattachement société <-> cadre), consultation de tous les plannings, validation d'une société entière une fois tous ses cadres validés, réouverture en cas de correction, export PDF et Excel par société/mois.
This commit is contained in:
27 files changed
+5492
No files matched your search
@@ -0,0 +1,146 @@
|
||||
const express = require('express');
|
||||
const db = require('../db');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
function parseYearMonth(req) {
|
||||
const year = parseInt(req.body?.year ?? req.query?.year, 10);
|
||||
const month = parseInt(req.body?.month ?? req.query?.month, 10);
|
||||
if (!year || !month || month < 1 || month > 12) return null;
|
||||
return { year, month };
|
||||
}
|
||||
|
||||
// Cadre validates their own month.
|
||||
router.post('/cadre', async (req, res) => {
|
||||
const ym = parseYearMonth(req);
|
||||
if (!ym) return res.status(400).json({ error: 'year/month invalides' });
|
||||
|
||||
// Admin can validate on behalf of a cadre (e.g. corrections), otherwise self only.
|
||||
const targetUserId = req.user.role === 'admin' && req.body.user_id ? req.body.user_id : req.user.id;
|
||||
|
||||
const { rows: userRows } = await db.query('SELECT id, company_id FROM users WHERE id = $1', [targetUserId]);
|
||||
const target = userRows[0];
|
||||
if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' });
|
||||
|
||||
const { rows: cv } = await db.query(
|
||||
'SELECT admin_validated FROM company_month_validations WHERE company_id = $1 AND year = $2 AND month = $3',
|
||||
[target.company_id, ym.year, ym.month]
|
||||
);
|
||||
if (cv[0]?.admin_validated) {
|
||||
return res.status(423).json({ error: 'La société a déjà été validée par l’administrateur pour ce mois' });
|
||||
}
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO month_locks (user_id, year, month, cadre_validated, cadre_validated_at)
|
||||
VALUES ($1, $2, $3, true, now())
|
||||
ON CONFLICT (user_id, year, month)
|
||||
DO UPDATE SET cadre_validated = true, cadre_validated_at = now()`,
|
||||
[target.id, ym.year, ym.month]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Admin reopens a cadre's month (allows corrections again).
|
||||
router.post('/cadre/reopen', requireAdmin, async (req, res) => {
|
||||
const ym = parseYearMonth(req);
|
||||
const { user_id } = req.body || {};
|
||||
if (!ym || !user_id) return res.status(400).json({ error: 'Paramètres invalides' });
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO month_locks (user_id, year, month, cadre_validated, cadre_validated_at)
|
||||
VALUES ($1, $2, $3, false, NULL)
|
||||
ON CONFLICT (user_id, year, month)
|
||||
DO UPDATE SET cadre_validated = false, cadre_validated_at = NULL`,
|
||||
[user_id, ym.year, ym.month]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Admin: status of every cadre in a company for a given month.
|
||||
router.get('/company/:companyId', requireAdmin, async (req, res) => {
|
||||
const year = parseInt(req.query.year, 10);
|
||||
const month = parseInt(req.query.month, 10);
|
||||
const companyId = req.params.companyId;
|
||||
if (!year || !month) return res.status(400).json({ error: 'year et month requis' });
|
||||
|
||||
const { rows: cadres } = await db.query(
|
||||
`SELECT u.id, u.full_name, u.email,
|
||||
ml.cadre_validated, ml.cadre_validated_at
|
||||
FROM users u
|
||||
LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $2 AND ml.month = $3
|
||||
WHERE u.company_id = $1 AND u.role = 'cadre' AND u.active = true
|
||||
ORDER BY u.full_name`,
|
||||
[companyId, year, month]
|
||||
);
|
||||
|
||||
const { rows: cv } = await db.query(
|
||||
`SELECT admin_validated, admin_validated_at, admin_validated_by
|
||||
FROM company_month_validations WHERE company_id = $1 AND year = $2 AND month = $3`,
|
||||
[companyId, year, month]
|
||||
);
|
||||
|
||||
res.json({
|
||||
cadres: cadres.map((c) => ({
|
||||
id: c.id,
|
||||
fullName: c.full_name,
|
||||
email: c.email,
|
||||
cadreValidated: !!c.cadre_validated,
|
||||
cadreValidatedAt: c.cadre_validated_at,
|
||||
})),
|
||||
allCadresValidated: cadres.length > 0 && cadres.every((c) => c.cadre_validated),
|
||||
companyValidated: !!cv[0]?.admin_validated,
|
||||
companyValidatedAt: cv[0]?.admin_validated_at || null,
|
||||
});
|
||||
});
|
||||
|
||||
// Admin validates the whole company for the month (requires every cadre validated).
|
||||
router.post('/company/:companyId', requireAdmin, async (req, res) => {
|
||||
const ym = parseYearMonth(req);
|
||||
const companyId = req.params.companyId;
|
||||
if (!ym) return res.status(400).json({ error: 'year/month invalides' });
|
||||
|
||||
const { rows: cadres } = await db.query(
|
||||
`SELECT u.id, u.full_name, ml.cadre_validated
|
||||
FROM users u
|
||||
LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $2 AND ml.month = $3
|
||||
WHERE u.company_id = $1 AND u.role = 'cadre' AND u.active = true`,
|
||||
[companyId, ym.year, ym.month]
|
||||
);
|
||||
|
||||
const pending = cadres.filter((c) => !c.cadre_validated);
|
||||
if (pending.length > 0) {
|
||||
return res.status(409).json({
|
||||
error: 'Tous les cadres doivent valider leur mois avant validation de la société',
|
||||
pending: pending.map((c) => c.full_name),
|
||||
});
|
||||
}
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO company_month_validations (company_id, year, month, admin_validated, admin_validated_at, admin_validated_by)
|
||||
VALUES ($1, $2, $3, true, now(), $4)
|
||||
ON CONFLICT (company_id, year, month)
|
||||
DO UPDATE SET admin_validated = true, admin_validated_at = now(), admin_validated_by = $4`,
|
||||
[companyId, ym.year, ym.month, req.user.id]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Admin reopens a company's month validation.
|
||||
router.post('/company/:companyId/reopen', requireAdmin, async (req, res) => {
|
||||
const ym = parseYearMonth(req);
|
||||
const companyId = req.params.companyId;
|
||||
if (!ym) return res.status(400).json({ error: 'year/month invalides' });
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO company_month_validations (company_id, year, month, admin_validated, admin_validated_at, admin_validated_by)
|
||||
VALUES ($1, $2, $3, false, NULL, NULL)
|
||||
ON CONFLICT (company_id, year, month)
|
||||
DO UPDATE SET admin_validated = false, admin_validated_at = NULL, admin_validated_by = NULL`,
|
||||
[companyId, ym.year, ym.month]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in new issue
Block a user