mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-11 17:27:42 +02:00
Implémenter le design Presencia (projet Claude Design)
Reprend la maquette Presencia.dc.html sur l'application réelle : thème clair chaud à accent vert forêt, navigation latérale groupée par rôle et tableau de bord, en conservant l'intégralité du fonctionnel existant. Frontend - Écran de connexion en deux panneaux (argumentaire + formulaire). - Tableau de bord par rôle : compteurs du mois et liste « à faire maintenant » (sociétés prêtes à valider, cadres en retard, saisie à compléter), plus l'état du mois par société côté admin. - Planning : saisie rapide au pinceau, trois affichages (grille, compact, liste), sélecteur avec raccourcis clavier (1–4, ⌫, ↵ pour appliquer à la journée), barre d'état de verrouillage, « Tout effacer ». - Nouvelle vue Historique pour les cadres (totaux par mois et statut). - Validation & export : barre de progression et compteurs de demi-journées par cadre. - Mobile : liste verticale, navigation en barre basse, cibles 44 px. Backend - GET /api/validations/overview — état de toutes les sociétés d'un mois. - GET /api/attendance/history — totaux par mois et par statut. - DELETE /api/attendance/month — effacement d'un mois (mêmes verrous). - Compteur de demi-journées saisies par cadre dans /validations/company. Polices et icônes servies localement (Inter, sous-ensemble de 18 icônes Phosphor) au lieu de Google Fonts et unpkg : l'application ne fait plus aucune requête externe au chargement. Vérifié de bout en bout via la vraie configuration nginx : 62 contrôles Playwright (desktop et mobile), tous passants, sur base réinitialisée et rejoués deux fois pour garantir le déterminisme.
This commit is contained in:
14 files changed
+2024
-1225
No files matched your search
@@ -6,11 +6,15 @@ Application de gestion des présences par demi-journée, multi-sociétés, avec
|
||||
|
||||
- **Connexion uniquement** : aucune inscription publique. Tous les comptes (admin et cadres) sont créés par un administrateur.
|
||||
- **Multi-sociétés** : chaque cadre est rattaché à une société. L'administrateur voit toutes les sociétés et tous les plannings ; un cadre ne voit que le sien.
|
||||
- **Planning mensuel** : vue calendrier, clic sur une demi-journée (matin/après-midi) pour choisir : Présent, Absent, Congé, RTT.
|
||||
- **Tableau de bord** adapté au rôle : compteurs du mois et raccourcis « à faire maintenant » (sociétés prêtes à valider, cadres en retard, saisie à compléter).
|
||||
- **Planning mensuel** : clic sur une demi-journée (matin/après-midi) pour choisir Présent, Non-présent, Congé ou RTT. Trois affichages au choix (grille, compact, liste), **saisie rapide au pinceau** (on active un statut puis on peint les cases), **raccourcis clavier** dans le sélecteur (1–4 pour les statuts, ⌫ pour effacer, ↵ pour appliquer à la journée entière), et deux actions groupées : « Remplir les jours ouvrés » et « Tout effacer ».
|
||||
- **Validation de fin de mois en deux temps** :
|
||||
1. Chaque cadre **valide son propre mois** (verrouille ses saisies).
|
||||
2. Une fois **tous les cadres d'une société** validés, l'administrateur **valide la société** pour ce mois (verrouillage complet). L'administrateur peut réouvrir un mois cadre ou une société en cas de correction nécessaire.
|
||||
- **Historique** : chaque cadre retrouve ses mois précédents avec les totaux par statut.
|
||||
- **Export** PDF et Excel par société et par mois (détail par cadre, jour et demi-journée).
|
||||
- **Interface mobile** : sous 900 px, le calendrier devient une liste verticale, la navigation passe en barre basse et les cibles tactiles font 44 px.
|
||||
- **Aucune dépendance externe au chargement** : la police (Inter) et les icônes (Phosphor) sont servies par l'application elle-même — aucune requête vers un CDN ou vers Google Fonts.
|
||||
- **Docker** : Postgres + API Node/Express + frontend Nginx, sur des ports non standards.
|
||||
|
||||
## Démarrage
|
||||
@@ -46,17 +50,32 @@ Si vous changez les identifiants admin dans `docker-compose.yml` *après* un pre
|
||||
|
||||
### En tant que cadre
|
||||
|
||||
- Renseigner sa présence par demi-journée sur le mois en cours (ou les mois précédents/suivants).
|
||||
- En fin de mois, cliquer sur **"Valider mon mois"** : les saisies sont alors verrouillées et transmises pour validation à l'administrateur. Si une correction est nécessaire après coup, il faut qu'un administrateur réouvre le mois.
|
||||
- Renseigner sa présence par demi-journée sur le mois en cours (ou les mois précédents/suivants). Le plus rapide : cliquer **« Remplir les jours ouvrés »** puis ne corriger que les exceptions (congés, RTT, absences).
|
||||
- Pour saisir plusieurs cases d'affilée, activer un statut dans **Saisie rapide** : chaque clic applique directement ce statut, sans passer par le sélecteur.
|
||||
- Dans le sélecteur d'une demi-journée : touches **1** à **4** pour les statuts, **⌫** pour effacer, **↵** pour appliquer à la journée entière.
|
||||
- En fin de mois, cliquer sur **« Valider mon mois »** : les saisies sont alors verrouillées et transmises pour validation à l'administrateur. Si une correction est nécessaire après coup, il faut qu'un administrateur réouvre le mois.
|
||||
- **Historique** : retrouver les mois précédents et leurs totaux, et les rouvrir en lecture.
|
||||
|
||||
## Architecture technique
|
||||
|
||||
```
|
||||
backend/ API Node.js / Express, PostgreSQL (pg), auth par cookie JWT httpOnly
|
||||
frontend/ Page HTML unique (vanilla JS), servie par Nginx qui proxifie /api vers le backend
|
||||
public/css/styles.css tokens et composants du design system (Nocturne)
|
||||
public/css/presencia.css couche applicative du design (calendrier, navigation, statuts)
|
||||
public/css/app.css glue : mises en page que la maquette exprimait en styles inline
|
||||
public/css/inter.css police Inter, servie localement
|
||||
public/css/phosphor.css sous-ensemble des 18 icônes utilisées, servi localement
|
||||
docker-compose.yml
|
||||
```
|
||||
|
||||
`styles.css` et `presencia.css` proviennent du projet Claude Design et sont à
|
||||
remplacer tels quels lors d'une nouvelle exportation du design ; les
|
||||
adaptations propres à l'application vivent dans `app.css` pour que cette
|
||||
réimportation reste triviale. Seule modification apportée à `styles.css` :
|
||||
l'`@import` vers Google Fonts a été retiré au profit de `inter.css` servi
|
||||
localement.
|
||||
|
||||
### Modèle de données (PostgreSQL)
|
||||
|
||||
- `companies` — sociétés
|
||||
|
||||
@@ -117,6 +117,83 @@ router.put('/', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Per-month aggregates for the history view: half-day counts by status plus
|
||||
// both validation flags, most recent month first.
|
||||
router.get('/history', async (req, res) => {
|
||||
const requestedUserId = req.query.user_id;
|
||||
if (requestedUserId && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Accès refusé' });
|
||||
}
|
||||
const target = await getTargetUser(req, requestedUserId);
|
||||
if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' });
|
||||
|
||||
const { rows } = await db.query(
|
||||
`WITH months AS (
|
||||
SELECT EXTRACT(YEAR FROM entry_date)::int AS year,
|
||||
EXTRACT(MONTH FROM entry_date)::int AS month,
|
||||
COUNT(*) FILTER (WHERE status = 'present') AS present,
|
||||
COUNT(*) FILTER (WHERE status = 'absent') AS absent,
|
||||
COUNT(*) FILTER (WHERE status = 'conge') AS conge,
|
||||
COUNT(*) FILTER (WHERE status = 'rtt') AS rtt
|
||||
FROM attendance_entries
|
||||
WHERE user_id = $1
|
||||
GROUP BY 1, 2
|
||||
)
|
||||
SELECT m.*, COALESCE(ml.cadre_validated, false) AS cadre_validated,
|
||||
COALESCE(cmv.admin_validated, false) AS company_validated
|
||||
FROM months m
|
||||
LEFT JOIN month_locks ml ON ml.user_id = $1 AND ml.year = m.year AND ml.month = m.month
|
||||
LEFT JOIN users u ON u.id = $1
|
||||
LEFT JOIN company_month_validations cmv
|
||||
ON cmv.company_id = u.company_id AND cmv.year = m.year AND cmv.month = m.month
|
||||
ORDER BY m.year DESC, m.month DESC
|
||||
LIMIT 24`,
|
||||
[target.id]
|
||||
);
|
||||
|
||||
res.json(rows.map((r) => ({
|
||||
year: r.year,
|
||||
month: r.month,
|
||||
present: parseInt(r.present, 10),
|
||||
absent: parseInt(r.absent, 10),
|
||||
conge: parseInt(r.conge, 10),
|
||||
rtt: parseInt(r.rtt, 10),
|
||||
cadreValidated: r.cadre_validated,
|
||||
companyValidated: r.company_validated,
|
||||
})));
|
||||
});
|
||||
|
||||
// Clear every entry of a month (the « Tout effacer » action). Same lock rules
|
||||
// as writes.
|
||||
router.delete('/month', async (req, res) => {
|
||||
const { year, month, user_id: requestedUserId } = req.body || {};
|
||||
const y = parseInt(year, 10);
|
||||
const m = parseInt(month, 10);
|
||||
if (!y || !m || m < 1 || m > 12) return res.status(400).json({ error: 'Paramètres invalides' });
|
||||
if (requestedUserId && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Accès refusé' });
|
||||
}
|
||||
const target = await getTargetUser(req, requestedUserId);
|
||||
if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' });
|
||||
|
||||
const lock = await getMonthLock(target.id, y, m);
|
||||
const companyValidation = await getCompanyValidation(target.company_id, y, m);
|
||||
if (companyValidation.admin_validated) {
|
||||
return res.status(423).json({ error: 'Ce mois a été validé par l’administrateur et est verrouillé' });
|
||||
}
|
||||
if (req.user.role !== 'admin' && lock.cadre_validated) {
|
||||
return res.status(423).json({ error: 'Vous avez déjà validé ce mois. Contactez un administrateur pour le modifier.' });
|
||||
}
|
||||
|
||||
const start = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
await db.query(
|
||||
`DELETE FROM attendance_entries
|
||||
WHERE user_id = $1 AND entry_date >= $2::date AND entry_date < ($2::date + INTERVAL '1 month')`,
|
||||
[target.id, start]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Bulk upsert (e.g. « fill all empty weekdays with présent »). All entries
|
||||
// must pass the same lock checks as single writes; months are checked once
|
||||
// per distinct month present in the payload.
|
||||
|
||||
@@ -65,14 +65,19 @@ router.get('/company/:companyId', requireAdmin, async (req, res) => {
|
||||
const companyId = req.params.companyId;
|
||||
if (!year || !month) return res.status(400).json({ error: 'year et month requis' });
|
||||
|
||||
const start = `${year}-${String(month).padStart(2, '0')}-01`;
|
||||
const { rows: cadres } = await db.query(
|
||||
`SELECT u.id, u.full_name, u.email,
|
||||
ml.cadre_validated, ml.cadre_validated_at
|
||||
ml.cadre_validated, ml.cadre_validated_at,
|
||||
COUNT(ae.id) AS filled
|
||||
FROM users u
|
||||
LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $2 AND ml.month = $3
|
||||
LEFT JOIN attendance_entries ae ON ae.user_id = u.id
|
||||
AND ae.entry_date >= $4::date AND ae.entry_date < ($4::date + INTERVAL '1 month')
|
||||
WHERE u.company_id = $1 AND u.role = 'cadre' AND u.active = true
|
||||
GROUP BY u.id, ml.cadre_validated, ml.cadre_validated_at
|
||||
ORDER BY u.full_name`,
|
||||
[companyId, year, month]
|
||||
[companyId, year, month, start]
|
||||
);
|
||||
|
||||
const { rows: cv } = await db.query(
|
||||
@@ -86,6 +91,7 @@ router.get('/company/:companyId', requireAdmin, async (req, res) => {
|
||||
id: c.id,
|
||||
fullName: c.full_name,
|
||||
email: c.email,
|
||||
filled: parseInt(c.filled, 10),
|
||||
cadreValidated: !!c.cadre_validated,
|
||||
cadreValidatedAt: c.cadre_validated_at,
|
||||
})),
|
||||
@@ -95,6 +101,42 @@ router.get('/company/:companyId', requireAdmin, async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Admin: one-shot overview of every company for a given month (dashboard).
|
||||
router.get('/overview', requireAdmin, async (req, res) => {
|
||||
const year = parseInt(req.query.year, 10);
|
||||
const month = parseInt(req.query.month, 10);
|
||||
if (!year || !month) return res.status(400).json({ error: 'year et month requis' });
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT c.id, c.name,
|
||||
COUNT(u.id) FILTER (WHERE u.id IS NOT NULL) AS active_cadres,
|
||||
COUNT(u.id) FILTER (WHERE ml.cadre_validated) AS validated_cadres,
|
||||
BOOL_OR(cmv.admin_validated) AS company_validated
|
||||
FROM companies c
|
||||
LEFT JOIN users u ON u.company_id = c.id AND u.role = 'cadre' AND u.active = true
|
||||
LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $1 AND ml.month = $2
|
||||
LEFT JOIN company_month_validations cmv ON cmv.company_id = c.id AND cmv.year = $1 AND cmv.month = $2
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name`,
|
||||
[year, month]
|
||||
);
|
||||
|
||||
const companies = rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
activeCadres: parseInt(r.active_cadres, 10),
|
||||
validatedCadres: parseInt(r.validated_cadres, 10),
|
||||
companyValidated: !!r.company_validated,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
companies,
|
||||
totalActiveCadres: companies.reduce((n, c) => n + c.activeCadres, 0),
|
||||
lateCadres: companies.reduce((n, c) => n + (c.companyValidated ? 0 : c.activeCadres - c.validatedCadres), 0),
|
||||
pendingCompanies: companies.filter((c) => !c.companyValidated).length,
|
||||
});
|
||||
});
|
||||
|
||||
// Admin validates the whole company for the month (requires every cadre validated).
|
||||
router.post('/company/:companyId', requireAdmin, async (req, res) => {
|
||||
const ym = parseYearMonth(req);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/* Presencia app glue: the layout the design expressed as inline styles on the
|
||||
.dc mockup, plus the few behaviours a static mockup has no need for
|
||||
(hidden toggling, toast animation, dialog forms). Loaded last.
|
||||
Design tokens and component classes live in styles.css / presencia.css. */
|
||||
|
||||
/* Any element toggled via the `hidden` attribute must stay hidden even when a
|
||||
class on it sets a display value. */
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
body { margin: 0; }
|
||||
|
||||
/* ── brand ── */
|
||||
.brand-row { display: flex; align-items: center; gap: 9px; }
|
||||
.brand-mark { width: 22px; height: 22px; border-radius: 6px; background: var(--color-accent); display: block; flex: none; }
|
||||
.brand-mark.small { width: 20px; height: 20px; }
|
||||
.brand-mark.tiny { width: 18px; height: 18px; border-radius: 5px; }
|
||||
.brand-name { font-size: 18px; font-weight: 500; letter-spacing: -0.01em; }
|
||||
.brand-name.small { font-size: 16px; }
|
||||
|
||||
/* ── login ── */
|
||||
.login-split { min-height: 100vh; display: grid; grid-template-columns: 1.1fr 1fr; background: var(--color-bg); }
|
||||
.login-pitch { display: flex; flex-direction: column; justify-content: space-between; padding: 44px 48px; border-right: 1px solid var(--color-divider); }
|
||||
.login-pitch-copy { max-width: 420px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.login-pitch-copy h1 { font-size: 40px; margin: 0; }
|
||||
.login-pitch-copy p { font-size: 15px; margin: 0; color: var(--color-neutral-700); }
|
||||
.login-pitch-feet { display: flex; gap: 26px; font-size: 12px; color: var(--color-neutral-700); flex-wrap: wrap; }
|
||||
.login-formside { display: grid; place-items: center; padding: 32px; }
|
||||
.login-form { width: min(360px, 100%); display: flex; flex-direction: column; gap: 16px; }
|
||||
.login-form h3 { margin: 0 0 4px; }
|
||||
.login-lede { font-size: 13px; margin: 0; color: var(--color-neutral-700); }
|
||||
.login-error { font-size: 13px; padding: 9px 11px; border-radius: var(--radius-md); background: #fbeae6; color: #a8412c; }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.login-split { grid-template-columns: 1fr; }
|
||||
.login-pitch { border-right: 0; border-bottom: 1px solid var(--color-divider); padding: 24px 20px; gap: 20px; }
|
||||
.login-pitch-copy h1 { font-size: 28px; }
|
||||
.login-pitch-feet { display: none; }
|
||||
.login-formside { padding: 24px 20px 40px; place-items: start center; }
|
||||
}
|
||||
|
||||
/* ── sidebar ── */
|
||||
.side-brand { display: flex; align-items: center; gap: 9px; padding: 4px 10px 2px; }
|
||||
.side nav { display: flex; flex-direction: column; gap: 14px; }
|
||||
.side-foot { margin-top: auto; display: flex; flex-direction: column; gap: 8px; padding: 0 4px; }
|
||||
.side-user { display: flex; align-items: center; gap: 9px; padding: 8px 6px; border-top: 1px solid var(--color-divider); }
|
||||
.avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--color-accent-100); color: var(--color-accent-700); display: grid; place-items: center; font-size: 12px; flex: none; }
|
||||
.avatar.neutral { background: var(--color-neutral-200); color: var(--color-neutral-700); font-size: 11px; }
|
||||
.side-user-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
|
||||
.side-user-name { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.side-user-meta { font-size: 11px; color: var(--color-neutral-700); }
|
||||
|
||||
/* ── mobile chrome ── */
|
||||
.mobile-head { align-items: center; gap: 10px; margin-bottom: 14px; }
|
||||
.mobile-title { font-size: 15px; font-weight: 500; margin-right: auto; }
|
||||
.mobile-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; background: var(--color-surface); border-top: 1px solid var(--color-divider); padding: 6px 8px; justify-content: space-around; }
|
||||
.mobile-nav .navitem { flex-direction: column; gap: 3px; justify-content: center; text-align: center; padding: 6px 4px; font-size: 10px; }
|
||||
.mobile-nav .navitem .ph { font-size: 19px; }
|
||||
|
||||
/* ── generic stacks / heads ── */
|
||||
.stack-20 { display: flex; flex-direction: column; gap: 20px; }
|
||||
.stack-14 { display: flex; flex-direction: column; gap: 14px; }
|
||||
.stack-8 { display: flex; flex-direction: column; gap: 8px; }
|
||||
.stack-6 { display: flex; flex-direction: column; gap: 6px; }
|
||||
.lede { font-size: 13px; margin: 0; color: var(--color-neutral-700); }
|
||||
.view h3 { margin: 0 0 2px; }
|
||||
.view h6 { margin: 0; }
|
||||
.page-head { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
|
||||
.page-head > div { margin-right: auto; }
|
||||
.table-card { padding: 6px 10px 10px; }
|
||||
.filter-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
||||
.filter-bar .field { min-width: 200px; }
|
||||
.cell-muted { color: var(--color-neutral-700); }
|
||||
.cell-actions { text-align: right; white-space: nowrap; }
|
||||
.empty-row td { color: var(--color-neutral-700); font-size: 13px; }
|
||||
|
||||
/* ── dashboard ── */
|
||||
.tile-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
|
||||
.tile-label { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-neutral-700); }
|
||||
.tile-value { font-size: 26px; font-weight: 500; line-height: 1.15; }
|
||||
.tile-hint { font-size: 12px; color: var(--color-neutral-700); }
|
||||
.rowbtn-ico { font-size: 17px; color: var(--color-accent-600); flex: none; }
|
||||
.rowbtn-text { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; text-align: left; }
|
||||
.rowbtn-sub { font-size: 12px; color: var(--color-neutral-700); }
|
||||
.rowbtn-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
||||
.rowbtn-right .ph-arrow-right { font-size: 14px; opacity: .5; }
|
||||
|
||||
/* ── planning ── */
|
||||
.planning-head { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
|
||||
.planning-head-title { margin-right: auto; }
|
||||
.back-link { margin-bottom: 4px; padding-inline: 0; }
|
||||
.month-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.month-label { font-size: 14px; font-weight: 500; min-width: 132px; text-align: center; text-transform: capitalize; }
|
||||
|
||||
.lockbar { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; background: var(--color-surface); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-text) 7%, transparent); flex-wrap: wrap; }
|
||||
.lockbar-icon { font-size: 17px; color: var(--color-accent-600); }
|
||||
.lockbar-text { display: flex; flex-direction: column; line-height: 1.35; }
|
||||
.lockbar-title { font-size: 13px; font-weight: 500; }
|
||||
.lockbar-detail { font-size: 12px; color: var(--color-neutral-700); }
|
||||
.lockbar-actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.toolrow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.toolrow-label { font-size: 12px; color: var(--color-neutral-700); margin-right: 2px; }
|
||||
.toolrow-items { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.toolrow-right { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.brush-hint { font-size: 12px; color: var(--color-neutral-700); }
|
||||
.view-badge { font-size: 10px; letter-spacing: .08em; opacity: .6; }
|
||||
|
||||
.day-head { display: flex; align-items: baseline; gap: 5px; }
|
||||
.day-num { font-size: 12px; font-weight: 600; }
|
||||
.day-wd { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--color-neutral-700); }
|
||||
|
||||
.summary-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.summary-chip { display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 8px; background: var(--color-surface); font-size: 12px; }
|
||||
.summary-chip-value { font-weight: 500; }
|
||||
.summary-chip-label { color: var(--color-neutral-700); }
|
||||
|
||||
/* ── validation ── */
|
||||
.validation-card { padding: 14px; gap: 12px; }
|
||||
.validation-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
|
||||
.validation-head-text { display: flex; flex-direction: column; line-height: 1.4; }
|
||||
.validation-company { font-size: 17px; font-weight: 500; }
|
||||
.validation-status { font-size: 12px; color: var(--color-neutral-700); }
|
||||
.validation-head-actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.progress { height: 6px; border-radius: 999px; background: color-mix(in srgb, var(--color-text) 10%, transparent); overflow: hidden; }
|
||||
.progress-bar { display: block; height: 100%; background: var(--color-accent); width: 0; transition: width .2s ease; }
|
||||
|
||||
/* ── popover ── */
|
||||
.pop-backdrop { position: fixed; inset: 0; z-index: 80; }
|
||||
.pop-head { display: flex; align-items: center; gap: 8px; padding: 4px 8px 6px; }
|
||||
.pop-title { font-size: 12px; font-weight: 500; }
|
||||
.pop-hint { margin-left: auto; font-size: 10px; color: var(--color-neutral-700); }
|
||||
.pop-opt-muted { color: var(--color-neutral-700); }
|
||||
.pop-ico { font-size: 14px; width: 9px; }
|
||||
|
||||
/* ── dialog ── */
|
||||
.dialog-backdrop { z-index: 130; }
|
||||
.dialog h4 { margin: 0; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
|
||||
.dialog .field + .field { margin-top: var(--space-3); }
|
||||
|
||||
/* ── toast ── */
|
||||
.toast {
|
||||
position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
|
||||
z-index: 140; padding: 10px 16px; border-radius: 8px;
|
||||
background: var(--color-surface); box-shadow: var(--shadow-lg); font-size: 13px;
|
||||
max-width: calc(100vw - 32px); text-align: center;
|
||||
/* never swallow clicks meant for the UI underneath */
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast.is-error { background: #fbeae6; color: #a8412c; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.toast { bottom: 76px; }
|
||||
.filter-bar .field { min-width: 0; flex: 1 1 160px; }
|
||||
.month-label { min-width: 104px; }
|
||||
.lockbar-actions { width: 100%; margin-left: 0; }
|
||||
.lockbar-actions .btn { flex: 1; }
|
||||
.toolrow-right { width: 100%; margin-left: 0; }
|
||||
.toolrow-right .btn { flex: 1; }
|
||||
}
|
||||
|
||||
.nav-group-items { display: flex; flex-direction: column; gap: 2px; }
|
||||
@@ -0,0 +1,75 @@
|
||||
/* Inter (SIL Open Font License 1.1) — vendored latin subsets so the app
|
||||
loads no external font and makes no request to Google. */
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/inter-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Phosphor Icons (MIT) — vendored subset: only the glyphs Presencia uses,
|
||||
so the app renders identically with no external CDN request. */
|
||||
@font-face {
|
||||
font-family: "Phosphor";
|
||||
src:
|
||||
url("../fonts/Phosphor.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.ph {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: "Phosphor" !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Enable Ligatures ================ */
|
||||
letter-spacing: 0;
|
||||
-webkit-font-feature-settings: "liga";
|
||||
-moz-font-feature-settings: "liga=1";
|
||||
-moz-font-feature-settings: "liga";
|
||||
-ms-font-feature-settings: "liga" 1;
|
||||
font-feature-settings: "liga";
|
||||
-webkit-font-variant-ligatures: discretionary-ligatures;
|
||||
font-variant-ligatures: discretionary-ligatures;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ph.ph-arrow-right:before {
|
||||
content: "\e06c";
|
||||
}
|
||||
.ph.ph-arrows-out-line-horizontal:before {
|
||||
content: "\e534";
|
||||
}
|
||||
.ph.ph-bell-ringing:before {
|
||||
content: "\e5e8";
|
||||
}
|
||||
.ph.ph-buildings:before {
|
||||
content: "\e102";
|
||||
}
|
||||
.ph.ph-calendar-dots:before {
|
||||
content: "\e7b4";
|
||||
}
|
||||
.ph.ph-caret-left:before {
|
||||
content: "\e138";
|
||||
}
|
||||
.ph.ph-caret-right:before {
|
||||
content: "\e13a";
|
||||
}
|
||||
.ph.ph-clock-counter-clockwise:before {
|
||||
content: "\e1a0";
|
||||
}
|
||||
.ph.ph-file-pdf:before {
|
||||
content: "\e702";
|
||||
}
|
||||
.ph.ph-file-xls:before {
|
||||
content: "\eb22";
|
||||
}
|
||||
.ph.ph-lock-simple:before {
|
||||
content: "\e308";
|
||||
}
|
||||
.ph.ph-pencil-simple-line:before {
|
||||
content: "\ebc6";
|
||||
}
|
||||
.ph.ph-plus:before {
|
||||
content: "\e3d4";
|
||||
}
|
||||
.ph.ph-seal-check:before {
|
||||
content: "\e606";
|
||||
}
|
||||
.ph.ph-sign-out:before {
|
||||
content: "\e42a";
|
||||
}
|
||||
.ph.ph-squares-four:before {
|
||||
content: "\e464";
|
||||
}
|
||||
.ph.ph-user-gear:before {
|
||||
content: "\e4cc";
|
||||
}
|
||||
.ph.ph-users-three:before {
|
||||
content: "\e68e";
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/* Presencia — app layer over the Nocturne tokens. Only what inline styles
|
||||
cannot express: status colors, responsive layout, hover/pseudo states. */
|
||||
|
||||
/* Light, natural retheme: warm off-white grounds, forest-green accent, no violet.
|
||||
Overrides the token sheet loaded before this file. */
|
||||
:root {
|
||||
--color-bg: #f4f3ef;
|
||||
--color-surface: #ffffff;
|
||||
--color-text: #1f2320;
|
||||
--color-accent: #3f6b54;
|
||||
--color-accent-2: #3f6b54;
|
||||
--color-divider: color-mix(in srgb, #1f2320 12%, transparent);
|
||||
|
||||
--color-accent-100: #eaf1ec;
|
||||
--color-accent-200: #d4e3d9;
|
||||
--color-accent-300: #b0cbbb;
|
||||
--color-accent-400: #7fa993;
|
||||
--color-accent-500: #5c8a72;
|
||||
--color-accent-600: #3f6b54;
|
||||
--color-accent-700: #325742;
|
||||
--color-accent-800: #274233;
|
||||
--color-accent-900: #1b2e24;
|
||||
|
||||
--color-accent-2-100: #eaf1ec;
|
||||
--color-accent-2-200: #d4e3d9;
|
||||
--color-accent-2-300: #b0cbbb;
|
||||
--color-accent-2-400: #7fa993;
|
||||
--color-accent-2-500: #5c8a72;
|
||||
--color-accent-2-600: #3f6b54;
|
||||
--color-accent-2-700: #325742;
|
||||
--color-accent-2-800: #274233;
|
||||
--color-accent-2-900: #1b2e24;
|
||||
|
||||
--color-neutral-100: #f8f8f6;
|
||||
--color-neutral-200: #ededea;
|
||||
--color-neutral-300: #dcdcd7;
|
||||
--color-neutral-400: #c2c2bc;
|
||||
--color-neutral-500: #a0a09a;
|
||||
--color-neutral-600: #7d7d77;
|
||||
--color-neutral-700: #5c5c57;
|
||||
--color-neutral-800: #40403c;
|
||||
--color-neutral-900: #2a2a27;
|
||||
|
||||
--shadow-sm: 0 0 0 1px rgba(31, 35, 32, 0.09);
|
||||
--shadow-md: 0 1px 2px rgba(31, 35, 32, 0.06), 0 6px 16px rgba(31, 35, 32, 0.08);
|
||||
--shadow-lg: 0 0 0 1px rgba(31, 35, 32, 0.06), 0 14px 36px rgba(31, 35, 32, 0.14);
|
||||
}
|
||||
|
||||
.tag-accent, .tag-accent-2 { background: var(--color-accent-100); color: var(--color-accent-700); }
|
||||
.tag-neutral { background: var(--color-neutral-200); color: var(--color-neutral-700); }
|
||||
.table th, .text-muted, .card-meta, figcaption, .field > label { color: var(--color-neutral-700); }
|
||||
|
||||
.shell { display: grid; grid-template-columns: 232px minmax(0, 1fr); min-height: 100vh; }
|
||||
.side { border-right: 1px solid var(--color-divider); padding: 14px 12px 24px; display: flex; flex-direction: column; gap: 18px; position: sticky; top: 0; height: 100vh; }
|
||||
.side-group-label { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-neutral-700); padding: 0 10px 6px; }
|
||||
.navitem { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px; border: 0; border-radius: 8px; background: transparent; color: var(--color-neutral-800); font: inherit; font-size: 14px; cursor: pointer; text-align: left; }
|
||||
.navitem:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); color: var(--color-text); }
|
||||
.navitem[aria-current="page"] { background: var(--color-accent-100); color: var(--color-accent-700); box-shadow: inset 2px 0 0 var(--color-accent); }
|
||||
.navitem .ni-badge { margin-left: auto; font-size: 11px; padding: 1px 7px; border-radius: 999px; background: var(--color-accent-600); color: #fff; }
|
||||
.navitem .ni-ico { width: 18px; text-align: center; opacity: .85; font-size: 15px; }
|
||||
|
||||
.content { padding: 22px 28px 72px; max-width: 1120px; min-width: 0; }
|
||||
.content .card:has(> .table), .content .card:has(> table.table) { overflow-x: auto; }
|
||||
.content table.table { min-width: 560px; }
|
||||
|
||||
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
|
||||
.cal-head { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--color-neutral-700); padding-bottom: 6px; }
|
||||
.day { background: var(--color-surface); border-radius: 8px; padding: 6px; display: flex; flex-direction: column; gap: 5px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||
.day.is-blank { background: transparent; box-shadow: none; }
|
||||
.day.is-weekend { background: var(--color-neutral-100); }
|
||||
.day.is-today { box-shadow: inset 0 0 0 1px var(--color-accent); }
|
||||
.halves { display: flex; gap: 4px; min-width: 0; }
|
||||
.half-lbl-long { display: none; }
|
||||
.pv-list .half-lbl-long { display: inline; }
|
||||
.pv-list .half-lbl-short { display: none; }
|
||||
.half { flex: 1 1 0; min-width: 0; overflow: hidden; min-height: 40px; border-radius: 6px; border: 1px dashed color-mix(in srgb, var(--color-text) 14%, transparent); background: transparent; color: var(--color-neutral-600); font: inherit; font-size: 10px; font-weight: 600; letter-spacing: .04em; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px; transition: transform .08s ease, filter .12s ease; }
|
||||
.half:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 1px 3px rgba(31,35,32,.12); }
|
||||
.half:disabled { cursor: default; }
|
||||
.half.st-present { border-style: solid; border-color: transparent; background: #e7f0e9; color: #2f6b45; }
|
||||
.half.st-absent { border-style: solid; border-color: transparent; background: #fbeae6; color: #a8412c; }
|
||||
.half.st-conge { border-style: solid; border-color: transparent; background: #fbf1e0; color: #8a5c14; }
|
||||
.half.st-rtt { border-style: solid; border-color: transparent; background: #e6eef3; color: #33607f; }
|
||||
|
||||
/* Compact direction — 1b */
|
||||
.pv-dense .day { padding: 4px; gap: 3px; }
|
||||
.pv-dense .halves { flex-direction: column; gap: 2px; }
|
||||
.pv-dense .half { min-height: 17px; font-size: 9px; }
|
||||
|
||||
/* List direction — 1c */
|
||||
.pv-list .cal-head { display: none; }
|
||||
.pv-list .cal-grid { display: flex; flex-direction: column; gap: 4px; }
|
||||
.pv-list .day { flex-direction: row; align-items: center; gap: 12px; padding: 7px 10px; }
|
||||
.pv-list .day.is-blank { display: none; }
|
||||
.pv-list .halves { flex: 1; max-width: 440px; }
|
||||
.pv-list .half { min-height: 44px; font-size: 11px; }
|
||||
|
||||
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; }
|
||||
.dot.st-present { background: #4b8a63; }
|
||||
.dot.st-absent { background: #c0563c; }
|
||||
.dot.st-conge { background: #b8862f; }
|
||||
.dot.st-rtt { background: #4a7a9b; }
|
||||
.dot.st-none { background: var(--color-neutral-400); }
|
||||
|
||||
.pop { position: fixed; z-index: 90; width: 214px; padding: 6px; border-radius: 10px; background: var(--color-surface); box-shadow: var(--shadow-lg); }
|
||||
.pop-opt { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 8px; border: 0; border-radius: 6px; background: transparent; color: var(--color-text); font: inherit; font-size: 14px; cursor: pointer; text-align: left; }
|
||||
.pop-opt:hover { background: color-mix(in srgb, var(--color-text) 8%, transparent); }
|
||||
.pop-opt .kbd { margin-left: auto; font-size: 10px; padding: 1px 5px; border-radius: 4px; color: var(--color-neutral-700); box-shadow: inset 0 0 0 1px var(--color-neutral-300); }
|
||||
|
||||
.btn, .chipbtn { white-space: nowrap; }
|
||||
.chipbtn { display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 8px; border: 1px solid var(--color-divider); background: transparent; color: var(--color-neutral-800); font: inherit; font-size: 13px; cursor: pointer; }
|
||||
.chipbtn:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||
.chipbtn[aria-pressed="true"] { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); border-color: var(--color-accent); }
|
||||
|
||||
.tile { background: var(--color-surface); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||
.rowbtn { display: flex; align-items: center; gap: 12px; width: 100%; padding: 11px 12px; border: 0; border-radius: 8px; background: var(--color-surface); color: var(--color-text); font: inherit; font-size: 14px; text-align: left; cursor: pointer; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||
.rowbtn:hover { background: color-mix(in srgb, var(--color-text) 10%, transparent); }
|
||||
|
||||
.mobile-only { display: none; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
.side { display: none; }
|
||||
.content { padding: 14px 14px 96px; }
|
||||
.mobile-only { display: flex; }
|
||||
.cal-head { display: none; }
|
||||
.cal-grid { display: flex; flex-direction: column; gap: 4px; }
|
||||
.day { flex-direction: row; align-items: center; gap: 12px; padding: 7px 10px; }
|
||||
.day.is-blank { display: none; }
|
||||
.halves { flex: 1; }
|
||||
.half-lbl-long { display: inline; }
|
||||
.half-lbl-short { display: none; }
|
||||
.half { min-height: 44px; font-size: 11px; }
|
||||
.pv-dense .halves, .pv-dense .half { flex-direction: row; min-height: 44px; font-size: 11px; }
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
:root {
|
||||
--color-primary: #3949ab;
|
||||
--color-primary-dark: #2c3593;
|
||||
--color-primary-light: #e8eaf6;
|
||||
--color-bg: #f4f5f9;
|
||||
--color-surface: #ffffff;
|
||||
--color-border: #e2e4ec;
|
||||
--color-text: #1c1f2a;
|
||||
--color-muted: #6b7080;
|
||||
|
||||
--color-present: #2e7d32;
|
||||
--color-present-bg: #e6f4ea;
|
||||
--color-absent: #c62828;
|
||||
--color-absent-bg: #fbe9e9;
|
||||
--color-conge: #ef6c00;
|
||||
--color-conge-bg: #fdf0e2;
|
||||
--color-rtt: #1565c0;
|
||||
--color-rtt-bg: #e6eefb;
|
||||
--color-empty: #c7c9d3;
|
||||
|
||||
--radius: 10px;
|
||||
--shadow: 0 1px 3px rgba(20, 22, 40, 0.08), 0 1px 2px rgba(20, 22, 40, 0.06);
|
||||
--shadow-lg: 0 12px 32px rgba(20, 22, 40, 0.16);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* Any element toggled via the `hidden` attribute must stay hidden even if a
|
||||
class on it (e.g. display:flex) would otherwise override the UA default. */
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
button { font-family: inherit; cursor: pointer; }
|
||||
input, select { font-family: inherit; }
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 8px; }
|
||||
.brand-mark {
|
||||
color: var(--color-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
.brand-name {
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ---------- Login ---------- */
|
||||
.login-screen {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #3949ab 0%, #1a237e 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
.login-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 40px 36px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
.login-card .brand { justify-content: center; margin-bottom: 6px; }
|
||||
.login-subtitle {
|
||||
text-align: center;
|
||||
color: var(--color-muted);
|
||||
margin: 0 0 28px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.field { display: block; margin-bottom: 16px; }
|
||||
.field span { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--color-text); }
|
||||
.field input {
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: #fbfbfd;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-light);
|
||||
background: #fff;
|
||||
}
|
||||
.login-hint { text-align: center; color: var(--color-muted); font-size: 12px; margin: 20px 0 0; }
|
||||
.form-error {
|
||||
background: var(--color-absent-bg);
|
||||
color: var(--color-absent);
|
||||
font-size: 13px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background .15s, box-shadow .15s, color .15s, opacity .15s;
|
||||
}
|
||||
.btn-primary { background: var(--color-primary); color: #fff; }
|
||||
.btn-primary:hover { background: var(--color-primary-dark); }
|
||||
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.btn-outline { background: #fff; color: var(--color-primary); border: 1px solid var(--color-primary); }
|
||||
.btn-outline:hover { background: var(--color-primary-light); }
|
||||
.btn-outline:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.btn-ghost { background: transparent; color: var(--color-muted); }
|
||||
.btn-ghost:hover { background: #eceef4; color: var(--color-text); }
|
||||
.btn-danger { background: #fff; color: var(--color-absent); border: 1px solid var(--color-absent); }
|
||||
.btn-danger:hover { background: var(--color-absent-bg); }
|
||||
.btn-sm { padding: 6px 12px; font-size: 13px; }
|
||||
.btn-block { width: 100%; }
|
||||
.icon-btn {
|
||||
border: none; background: #eef0f6; color: var(--color-text);
|
||||
width: 32px; height: 32px; border-radius: 8px; font-size: 18px; line-height: 1;
|
||||
}
|
||||
.icon-btn:hover { background: var(--color-primary-light); color: var(--color-primary); }
|
||||
|
||||
/* ---------- App shell ---------- */
|
||||
.app { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
.topbar {
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.topbar-nav { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||
.nav-btn {
|
||||
border: none; background: transparent; padding: 8px 14px; border-radius: 8px;
|
||||
font-size: 14px; font-weight: 600; color: var(--color-muted);
|
||||
}
|
||||
.nav-btn:hover { background: #eef0f6; color: var(--color-text); }
|
||||
.nav-btn.active { background: var(--color-primary-light); color: var(--color-primary); }
|
||||
|
||||
.topbar-user { display: flex; align-items: center; gap: 12px; }
|
||||
.user-chip { text-align: right; }
|
||||
.user-name { display: block; font-weight: 600; font-size: 14px; }
|
||||
.user-role-badge {
|
||||
display: inline-block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
|
||||
color: var(--color-primary); font-weight: 700;
|
||||
}
|
||||
|
||||
.main { flex: 1; padding: 28px 32px 60px; max-width: 1180px; width: 100%; margin: 0 auto; }
|
||||
.view-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
|
||||
.view-header h1 { font-size: 22px; margin: 0; }
|
||||
h1 { font-size: 22px; margin: 0 0 4px; }
|
||||
h2 { font-size: 17px; margin: 0 0 4px; }
|
||||
.muted { color: var(--color-muted); font-size: 13px; margin: 0; }
|
||||
|
||||
.card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.selector-card { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
|
||||
.selector-card label { font-size: 13px; font-weight: 600; color: var(--color-text); display: flex; flex-direction: column; gap: 6px; }
|
||||
.selector-card select {
|
||||
padding: 9px 10px; border-radius: 8px; border: 1px solid var(--color-border); font-size: 14px; min-width: 180px;
|
||||
}
|
||||
|
||||
/* ---------- Calendar ---------- */
|
||||
.calendar-header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; }
|
||||
.calendar-title-group h1 { margin-bottom: 8px; }
|
||||
.month-nav { display: flex; align-items: center; gap: 12px; }
|
||||
.month-label { font-size: 16px; font-weight: 700; min-width: 160px; text-align: center; text-transform: capitalize; }
|
||||
|
||||
.validation-banner {
|
||||
padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 600;
|
||||
background: #eef0f6; color: var(--color-muted); max-width: 360px;
|
||||
}
|
||||
.validation-banner.locked { background: var(--color-rtt-bg); color: var(--color-rtt); }
|
||||
.validation-banner.validated { background: var(--color-present-bg); color: var(--color-present); }
|
||||
|
||||
.legend { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 16px; font-size: 13px; color: var(--color-muted); }
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
||||
.dot-present { background: var(--color-present); }
|
||||
.dot-absent { background: var(--color-absent); }
|
||||
.dot-conge { background: var(--color-conge); }
|
||||
.dot-rtt { background: var(--color-rtt); }
|
||||
.dot-empty { background: var(--color-empty); }
|
||||
|
||||
.calendar-weekdays {
|
||||
display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px;
|
||||
font-size: 12px; font-weight: 700; color: var(--color-muted); text-transform: uppercase;
|
||||
padding: 0 2px; margin-bottom: 6px;
|
||||
}
|
||||
.calendar-grid {
|
||||
display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px;
|
||||
}
|
||||
.day-cell {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
min-height: 84px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.day-cell.empty { background: transparent; border: none; }
|
||||
.day-cell.weekend { background: #fafafc; }
|
||||
.day-number { font-size: 13px; font-weight: 700; color: var(--color-text); }
|
||||
.half-day-row { display: flex; gap: 4px; flex: 1; }
|
||||
.half-day {
|
||||
flex: 1;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--color-border);
|
||||
background: #fafbfd;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 34px;
|
||||
transition: transform .1s, box-shadow .1s;
|
||||
}
|
||||
.half-day:not(.readonly):hover { transform: translateY(-1px); box-shadow: var(--shadow); }
|
||||
.half-day.readonly { cursor: default; }
|
||||
.half-day.st-present { background: var(--color-present-bg); color: var(--color-present); border-style: solid; border-color: transparent; }
|
||||
.half-day.st-absent { background: var(--color-absent-bg); color: var(--color-absent); border-style: solid; border-color: transparent; }
|
||||
.half-day.st-conge { background: var(--color-conge-bg); color: var(--color-conge); border-style: solid; border-color: transparent; }
|
||||
.half-day.st-rtt { background: var(--color-rtt-bg); color: var(--color-rtt); border-style: solid; border-color: transparent; }
|
||||
|
||||
.calendar-actions { margin-top: 22px; display: flex; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
|
||||
|
||||
/* Highlight of the current day */
|
||||
.day-cell.today { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); }
|
||||
.day-cell.today .day-number { color: var(--color-primary); }
|
||||
|
||||
/* ---------- Monthly summary tiles ---------- */
|
||||
.month-summary {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.month-summary:empty { display: none; }
|
||||
.summary-tile {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 7px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
.summary-count { font-size: 18px; font-weight: 700; }
|
||||
.summary-label { font-size: 12px; color: var(--color-muted); font-weight: 600; }
|
||||
.summary-present .summary-count { color: var(--color-present); }
|
||||
.summary-absent .summary-count { color: var(--color-absent); }
|
||||
.summary-conge .summary-count { color: var(--color-conge); }
|
||||
.summary-rtt .summary-count { color: var(--color-rtt); }
|
||||
|
||||
/* ---------- Tables ---------- */
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.data-table th {
|
||||
text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .03em;
|
||||
color: var(--color-muted); padding: 10px 12px; border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
.data-table td { padding: 12px; border-bottom: 1px solid var(--color-border); vertical-align: middle; }
|
||||
.data-table tr:last-child td { border-bottom: none; }
|
||||
.filter-row { margin-bottom: 14px; }
|
||||
.filter-row select { padding: 7px 10px; border-radius: 8px; border: 1px solid var(--color-border); }
|
||||
|
||||
.badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.badge-admin { background: var(--color-primary-light); color: var(--color-primary); }
|
||||
.badge-cadre { background: #eef0f6; color: var(--color-muted); }
|
||||
.badge-active { background: var(--color-present-bg); color: var(--color-present); }
|
||||
.badge-inactive { background: var(--color-absent-bg); color: var(--color-absent); }
|
||||
.badge-validated { background: var(--color-present-bg); color: var(--color-present); }
|
||||
.badge-pending { background: #fdf0e2; color: var(--color-conge); }
|
||||
|
||||
.row-actions { display: flex; gap: 8px; }
|
||||
|
||||
/* ---------- Validation summary ---------- */
|
||||
.validation-summary-header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
|
||||
.validation-summary-actions { display: flex; gap: 10px; }
|
||||
.export-row { display: flex; align-items: center; gap: 12px; margin-top: 18px; font-size: 14px; font-weight: 600; color: var(--color-muted); }
|
||||
|
||||
/* ---------- Popover ---------- */
|
||||
.popover {
|
||||
position: absolute;
|
||||
background: var(--color-surface);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 8px;
|
||||
z-index: 60;
|
||||
min-width: 160px;
|
||||
}
|
||||
.popover-title { font-size: 12px; font-weight: 700; color: var(--color-muted); padding: 4px 8px 8px; }
|
||||
.popover-option {
|
||||
display: flex; align-items: center; gap: 8px; width: 100%;
|
||||
border: none; background: transparent; padding: 9px 8px; border-radius: 6px;
|
||||
font-size: 14px; text-align: left; font-weight: 600; color: var(--color-text);
|
||||
}
|
||||
.popover-option:hover { background: #eef0f6; }
|
||||
.popover-clear { color: var(--color-muted); }
|
||||
|
||||
/* ---------- Modal ---------- */
|
||||
.modal-backdrop {
|
||||
position: fixed; inset: 0; background: rgba(20, 22, 40, .45);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px;
|
||||
}
|
||||
.modal {
|
||||
background: var(--color-surface); border-radius: 14px; box-shadow: var(--shadow-lg);
|
||||
padding: 26px; width: 100%; max-width: 420px;
|
||||
}
|
||||
.modal h2 { margin-bottom: 18px; }
|
||||
.modal .field { margin-bottom: 14px; }
|
||||
.modal .field input, .modal .field select {
|
||||
width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--color-border); font-size: 14px;
|
||||
}
|
||||
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
|
||||
|
||||
/* ---------- Toasts ---------- */
|
||||
.toast-container {
|
||||
position: fixed; top: 18px; right: 18px; z-index: 200;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
/* Notifications must never block clicks on the UI underneath them. */
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
background: var(--color-text); color: #fff; padding: 12px 18px; border-radius: 8px;
|
||||
font-size: 14px; box-shadow: var(--shadow-lg); max-width: 320px; opacity: 0; transform: translateY(-8px);
|
||||
animation: toast-in .2s forwards;
|
||||
}
|
||||
.toast.toast-error { background: var(--color-absent); }
|
||||
.toast.toast-success { background: var(--color-present); }
|
||||
@keyframes toast-in { to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.view[hidden] { display: none; }
|
||||
|
||||
.day-weekday-mobile { display: none; }
|
||||
|
||||
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.main { padding: 16px 12px 50px; }
|
||||
.selector-card { flex-direction: column; align-items: stretch; }
|
||||
.topbar { padding: 10px 14px; }
|
||||
.topbar-user { gap: 8px; }
|
||||
}
|
||||
|
||||
/* Below this width the 7-column grid becomes unusable by touch, so the
|
||||
calendar switches to a single-column list: one full-width row per day. */
|
||||
@media (max-width: 640px) {
|
||||
.calendar-header { flex-direction: column; }
|
||||
.calendar-weekdays { display: none; }
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.day-cell {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-height: unset;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
.day-cell.empty { display: none; }
|
||||
.day-number {
|
||||
flex: 0 0 auto;
|
||||
width: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.day-weekday-mobile {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.half-day-row { gap: 8px; }
|
||||
.half-day { min-height: 44px; font-size: 11px; }
|
||||
|
||||
.calendar-actions { justify-content: stretch; }
|
||||
.calendar-actions .btn { width: 100%; }
|
||||
|
||||
.legend { gap: 10px 14px; font-size: 12px; }
|
||||
|
||||
.popover { max-width: calc(100vw - 16px); }
|
||||
|
||||
.modal { padding: 20px; }
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/* Nocturne — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
|
||||
/* Inter is loaded from css/inter.css (vendored locally) instead of the
|
||||
Google Fonts import this file ships with: the app must not call out to a
|
||||
third party at page load. */
|
||||
|
||||
:root {
|
||||
--color-bg: #161826;
|
||||
--color-surface: #232532;
|
||||
--color-text: #e9e9ed;
|
||||
--color-accent: #9184d9; /* review round 7 (Barron): the accent moves to
|
||||
the product's blurple — OKLCH hue 289.2, the hue of the app's own Pro
|
||||
accent (#9a8fe0, --om-accent-pro) — at the same lightness the steel
|
||||
accent held (L 0.660, so the documented ratios survive) and chroma
|
||||
in the product blurple's own range (its token sits at C 0.118; the
|
||||
accent takes 0.125, from the old steel's 0.030):
|
||||
the accent now reads as an accent rather than another neutral. */
|
||||
--color-accent-2: #a7a1db; /* the machine-derived stand-in follows: same
|
||||
hue, same L 0.734, chroma at the same 2:3 proportion it held */
|
||||
--color-divider: color-mix(in srgb, #e9e9ed 16%, transparent);
|
||||
|
||||
/* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
|
||||
same step of any role matches the others in visual value. */
|
||||
--color-neutral-100: #f3f5fe;
|
||||
--color-neutral-200: #e4e7f5;
|
||||
--color-neutral-300: #cfd3e5;
|
||||
--color-neutral-400: #b2b6ca;
|
||||
--color-neutral-500: #9397ab;
|
||||
--color-neutral-600: #75798c;
|
||||
--color-neutral-700: #595d6c;
|
||||
--color-neutral-800: #3f424d;
|
||||
--color-neutral-900: #292b31;
|
||||
|
||||
--color-accent-100: #f5f4ff;
|
||||
--color-accent-200: #e7e5fe;
|
||||
--color-accent-300: #d2cefd;
|
||||
--color-accent-400: #b5abfc;
|
||||
--color-accent-500: #968ae0;
|
||||
--color-accent-600: #796cbf;
|
||||
--color-accent-700: #5d5294;
|
||||
--color-accent-800: #423a6a;
|
||||
--color-accent-900: #2b2741;
|
||||
|
||||
--color-accent-2-100: #f5f4ff;
|
||||
--color-accent-2-200: #e7e5fe;
|
||||
--color-accent-2-300: #d2cefd;
|
||||
--color-accent-2-400: #b5afe8;
|
||||
--color-accent-2-500: #9690c9;
|
||||
--color-accent-2-600: #7972a9;
|
||||
--color-accent-2-700: #5c5783;
|
||||
--color-accent-2-800: #423e5d;
|
||||
--color-accent-2-900: #2b293a;
|
||||
|
||||
/* Deck section-divider ground — review round 3: saturation as presence.
|
||||
Dividers take a saturated deep indigo (hue 277, the neutral ramp's own
|
||||
family; C 0.094 — written when the accent held C 0.030, before the
|
||||
round-7 blurple raised it past the ground's) with a lighter bloom and
|
||||
a ghost tint of the same hue, while the slide grounds stay
|
||||
desaturated. Deck-scale fills only — not interface colors. */
|
||||
--color-section: #262a60;
|
||||
--color-section-glow: #353b80;
|
||||
--color-section-ghost: #4c5397;
|
||||
|
||||
--font-heading: "Inter", system-ui, sans-serif;
|
||||
--font-heading-weight: 500;
|
||||
--font-body: "Inter", system-ui, sans-serif;
|
||||
|
||||
--space-1: 2.8px;
|
||||
--space-2: 5.6px;
|
||||
--space-3: 8.4px;
|
||||
--space-4: 11.2px;
|
||||
--space-6: 16.8px;
|
||||
--space-8: 22.4px;
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 14px;
|
||||
|
||||
/* Elevation — derived from the ground: soft ink-tinted shadows on a
|
||||
light theme, a hairline edge + ambient darkness on a dark one. */
|
||||
--shadow-sm: 0 0 0 1px #3f424d;
|
||||
--shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0,0,0,0.55);
|
||||
--shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.65);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }
|
||||
|
||||
.lighten{mix-blend-mode:lighten;background-color:transparent}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
Components — built with the tokens above. Plain CSS
|
||||
on plain HTML: no JavaScript, no build step. Each class is documented in
|
||||
readme.md and demonstrated in foundations/ and components/.
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||
line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
|
||||
}
|
||||
h1 { font-size: 42px; }
|
||||
h2 { font-size: 32px; }
|
||||
h3 { font-size: 25px; }
|
||||
h4 { font-size: 20px; }
|
||||
h5 { font-size: 16px; }
|
||||
h6 { font-size: 13px; }
|
||||
h6 { letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
p { margin: 0 0 var(--space-3); }
|
||||
a { color: var(--color-accent); text-underline-offset: 3px; }
|
||||
img { display: block; max-width: 100%; }
|
||||
figure { margin: 0; }
|
||||
figcaption {
|
||||
font-size: 11px; margin-top: var(--space-1);
|
||||
color: color-mix(in srgb, var(--color-text) 55%, transparent);
|
||||
}
|
||||
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
|
||||
:focus { outline: none; }
|
||||
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
||||
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }
|
||||
|
||||
/* — rules — */
|
||||
/* Rules fade to transparent at both ends instead of stopping cleanly — a
|
||||
Nocturne signature. The ramp is 48px an end (one deck baseline unit),
|
||||
painted from the same divider token. Freestanding rules fade; box
|
||||
outlines, in-control separators, and short accent marks stay solid. */
|
||||
.hr {
|
||||
height: 1px; border: 0; margin: var(--space-4) 0;
|
||||
background: linear-gradient(to right,
|
||||
transparent, var(--color-divider) 48px,
|
||||
var(--color-divider) calc(100% - 48px), transparent);
|
||||
}
|
||||
|
||||
/* — buttons — */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
cursor: pointer; text-decoration: none;
|
||||
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||
font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —
|
||||
the pair sits side by side in sign-up rows */
|
||||
background: transparent; border: 1px solid transparent;
|
||||
padding: var(--space-2) calc(var(--space-3) * 1.2);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.btn svg { display: block; }
|
||||
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
|
||||
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
|
||||
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
|
||||
.btn-secondary { border-color: var(--color-divider); }
|
||||
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
|
||||
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
|
||||
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
|
||||
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
|
||||
.btn-icon { width: 36px; height: 36px; padding: 0; }
|
||||
.btn-block { width: 100%; margin-top: var(--space-2); }
|
||||
|
||||
/* — forms — */
|
||||
.field > label {
|
||||
display: block; font-size: 12px; margin-bottom: 5px;
|
||||
color: color-mix(in srgb, var(--color-text) 70%, transparent);
|
||||
}
|
||||
.input {
|
||||
width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;
|
||||
font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-divider); border-radius: var(--radius-md);
|
||||
}
|
||||
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
|
||||
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
|
||||
textarea.input { min-height: 90px; resize: vertical; }
|
||||
.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }
|
||||
.radio input, .seg-opt input {
|
||||
position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
|
||||
}
|
||||
.radio .dot {
|
||||
width: 16px; height: 16px; flex: none; border-radius: 50%;
|
||||
border: 1.5px solid var(--color-divider);
|
||||
}
|
||||
.radio:hover .dot { border-color: var(--color-accent); }
|
||||
.radio input:checked + .dot {
|
||||
border-color: var(--color-accent); background: var(--color-accent);
|
||||
box-shadow: inset 0 0 0 4px var(--color-bg);
|
||||
}
|
||||
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
||||
.seg {
|
||||
display: inline-flex; overflow: hidden;
|
||||
border: 1px solid var(--color-divider); border-radius: var(--radius-md);
|
||||
}
|
||||
.seg-opt {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 7px 12px; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
|
||||
.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
|
||||
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
||||
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }
|
||||
|
||||
/* — cards — */
|
||||
.card {
|
||||
display: flex; flex-direction: column; gap: var(--space-2);
|
||||
padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-surface);
|
||||
}
|
||||
.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
|
||||
.card-title {
|
||||
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||
font-size: 17px; line-height: 1.2;
|
||||
}
|
||||
.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }
|
||||
.card-meta {
|
||||
display: flex; align-items: center; gap: 6px; font-size: 11px;
|
||||
color: color-mix(in srgb, var(--color-text) 50%, transparent);
|
||||
}
|
||||
.elev-sm { box-shadow: var(--shadow-sm); }
|
||||
.elev-md { box-shadow: var(--shadow-md); }
|
||||
.elev-lg { box-shadow: var(--shadow-lg); }
|
||||
|
||||
/* — tags — */
|
||||
.tag {
|
||||
display: inline-flex; align-items: center; font-size: 11px;
|
||||
letter-spacing: 0.02em; padding: 3px 10px;
|
||||
border-radius: calc(var(--radius-md) * 0.75);
|
||||
}
|
||||
.tag-accent { background: var(--color-accent-800); color: var(--color-accent-100); }
|
||||
.tag-accent-2 { background: var(--color-accent-2-800); color: var(--color-accent-2-100); }
|
||||
.tag-neutral { background: var(--color-neutral-800); color: var(--color-neutral-100); }
|
||||
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }
|
||||
|
||||
/* — navigation — */
|
||||
.nav {
|
||||
display: flex; align-items: center; gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: none;
|
||||
}
|
||||
.nav-brand {
|
||||
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||
font-size: 18px; margin-right: auto;
|
||||
}
|
||||
.nav a { color: inherit; text-decoration: none; font-size: 14px; }
|
||||
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }
|
||||
|
||||
/* — tables — */
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.table th {
|
||||
text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
|
||||
color: color-mix(in srgb, var(--color-text) 60%, transparent);
|
||||
padding: var(--space-2); border-bottom: 1px solid transparent; /* keeps layout; the row paints the fading rule */
|
||||
}
|
||||
.table td {
|
||||
padding: var(--space-2);
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
/* Row rules as row-level strips so the end-fade spans the row, not each cell. */
|
||||
.table thead tr {
|
||||
background: linear-gradient(to right,
|
||||
transparent, var(--color-divider) 48px,
|
||||
var(--color-divider) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
|
||||
}
|
||||
.table tbody tr {
|
||||
background: linear-gradient(to right,
|
||||
transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
|
||||
color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
|
||||
}
|
||||
.table tbody tr:hover {
|
||||
/* the hover tint layers over the row rule, which keeps painting */
|
||||
background:
|
||||
linear-gradient(color-mix(in srgb, var(--color-text) 4%, transparent),
|
||||
color-mix(in srgb, var(--color-text) 4%, transparent)) no-repeat 0 0 / 100% 100%,
|
||||
linear-gradient(to right,
|
||||
transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
|
||||
color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
|
||||
}
|
||||
|
||||
/* — dialog — */
|
||||
.dialog-backdrop {
|
||||
position: fixed; inset: 0; display: grid; place-items: center;
|
||||
padding: var(--space-4);
|
||||
background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
|
||||
}
|
||||
.dialog {
|
||||
width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
|
||||
padding: var(--space-4); border-radius: var(--radius-lg);
|
||||
background: var(--color-surface); box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.dialog-title {
|
||||
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
||||
font-size: 20px;
|
||||
}
|
||||
.dialog-body { font-size: 14px; opacity: 0.85; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+241
-158
@@ -4,207 +4,290 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Presencia — Gestion des présences</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%233949ab'/%3E%3Ctext x='50' y='72' font-size='64' text-anchor='middle' fill='white' font-family='Arial' font-weight='bold'%3EP%3C/text%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%233f6b54'/%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="css/inter.css" />
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
<link rel="stylesheet" href="css/presencia.css" />
|
||||
<link rel="stylesheet" href="css/phosphor.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<!-- LOGIN VIEW -->
|
||||
<section id="view-login" class="login-screen">
|
||||
<div class="login-card">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">◆</span>
|
||||
<!-- ══════════════ LOGIN ══════════════ -->
|
||||
<section id="view-login" class="login-split">
|
||||
<div class="login-pitch">
|
||||
<div class="brand-row">
|
||||
<span class="brand-mark"></span>
|
||||
<span class="brand-name">Presencia</span>
|
||||
</div>
|
||||
<p class="login-subtitle">Gestion des présences par société</p>
|
||||
<form id="login-form" autocomplete="on">
|
||||
<label class="field">
|
||||
<span>Adresse e-mail</span>
|
||||
<input type="email" id="login-email" required autocomplete="username" placeholder="vous@societe.com" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Mot de passe</span>
|
||||
<input type="password" id="login-password" required autocomplete="current-password" placeholder="••••••••" />
|
||||
</label>
|
||||
<div id="login-error" class="form-error" hidden></div>
|
||||
<div class="login-pitch-copy">
|
||||
<h1>Les présences,<br />sans le tableur.</h1>
|
||||
<p class="text-muted">Saisie par demi-journée, validation en deux temps, export mensuel par société. Chaque cadre remplit son mois en moins de deux minutes.</p>
|
||||
</div>
|
||||
<div class="login-pitch-feet">
|
||||
<span>Validation cadre puis administrateur</span>
|
||||
<span>Export PDF & Excel</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-formside">
|
||||
<form id="login-form" class="login-form" autocomplete="on">
|
||||
<div>
|
||||
<h3>Connexion</h3>
|
||||
<p class="login-lede">Accès réservé — les comptes sont créés par un administrateur.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="login-email">Adresse e-mail</label>
|
||||
<input class="input" id="login-email" type="email" required autocomplete="username" placeholder="vous@societe.com" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="login-password">Mot de passe</label>
|
||||
<input class="input" id="login-password" type="password" required autocomplete="current-password" placeholder="••••••••" />
|
||||
</div>
|
||||
<div id="login-error" class="login-error" hidden></div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Se connecter</button>
|
||||
</form>
|
||||
<p class="login-hint">Accès réservé — comptes créés par l'administrateur.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MAIN APP -->
|
||||
<div id="app" class="app" hidden>
|
||||
<header class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<span class="brand-mark">◆</span>
|
||||
<span class="brand-name">Presencia</span>
|
||||
<!-- ══════════════ APP ══════════════ -->
|
||||
<div id="app" class="shell" hidden>
|
||||
<aside class="side">
|
||||
<div class="side-brand">
|
||||
<span class="brand-mark small"></span>
|
||||
<span class="brand-name small">Presencia</span>
|
||||
</div>
|
||||
<nav id="admin-nav" class="topbar-nav" hidden>
|
||||
<button class="nav-btn" data-view="calendar" data-admin-mode="own">Mon planning</button>
|
||||
<button class="nav-btn" data-view="companies">Sociétés</button>
|
||||
<button class="nav-btn" data-view="users">Utilisateurs</button>
|
||||
<button class="nav-btn" data-view="plannings">Plannings</button>
|
||||
<button class="nav-btn" data-view="validation">Validation & export</button>
|
||||
</nav>
|
||||
<div class="topbar-user">
|
||||
<div class="user-chip">
|
||||
<span id="user-name" class="user-name"></span>
|
||||
<span id="user-role-badge" class="user-role-badge"></span>
|
||||
<nav id="side-nav"></nav>
|
||||
<div class="side-foot">
|
||||
<div class="side-user">
|
||||
<span id="user-initials" class="avatar"></span>
|
||||
<span class="side-user-text">
|
||||
<span id="user-name" class="side-user-name"></span>
|
||||
<span id="user-meta" class="side-user-meta"></span>
|
||||
</span>
|
||||
</div>
|
||||
<button id="logout-btn" class="btn btn-ghost btn-sm">Déconnexion</button>
|
||||
<button id="logout-btn" class="navitem"><i class="ni-ico ph ph-sign-out"></i><span>Déconnexion</span></button>
|
||||
</div>
|
||||
</header>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<main class="content">
|
||||
<div class="mobile-only mobile-head">
|
||||
<span class="brand-mark tiny"></span>
|
||||
<span id="screen-title" class="mobile-title"></span>
|
||||
<button id="logout-btn-mobile" class="chipbtn">Quitter</button>
|
||||
</div>
|
||||
|
||||
<!-- CALENDAR VIEW (cadre self, or admin viewing a cadre) -->
|
||||
<section id="view-calendar" class="view">
|
||||
<div class="calendar-header">
|
||||
<div class="calendar-title-group">
|
||||
<h1 id="calendar-owner-name">Mon planning</h1>
|
||||
<!-- ─── DASHBOARD ─── -->
|
||||
<section id="view-dashboard" class="view stack-20">
|
||||
<div>
|
||||
<h3 id="dash-greeting"></h3>
|
||||
<p id="dash-subtitle" class="lede"></p>
|
||||
</div>
|
||||
<div id="dash-tiles" class="tile-grid"></div>
|
||||
<div class="stack-8">
|
||||
<h6>À faire maintenant</h6>
|
||||
<div id="dash-actions" class="stack-6"></div>
|
||||
</div>
|
||||
<div id="dash-company-block" class="stack-8" hidden>
|
||||
<h6>État du mois par société</h6>
|
||||
<div class="card table-card">
|
||||
<table class="table">
|
||||
<thead><tr><th>Société</th><th>Cadres validés</th><th>Statut</th><th></th></tr></thead>
|
||||
<tbody id="dash-company-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── PLANNING ─── -->
|
||||
<section id="view-planning" class="view stack-14" hidden>
|
||||
<div class="planning-head">
|
||||
<div class="planning-head-title">
|
||||
<button id="back-to-plannings" class="btn btn-ghost back-link" hidden>← Tous les plannings</button>
|
||||
<h3 id="planning-title"></h3>
|
||||
<p id="planning-subtitle" class="lede"></p>
|
||||
</div>
|
||||
<div class="month-nav">
|
||||
<button id="prev-month" class="icon-btn" aria-label="Mois précédent">‹</button>
|
||||
<button id="prev-month" class="btn btn-secondary btn-icon" aria-label="Mois précédent"><i class="ph ph-caret-left"></i></button>
|
||||
<span id="month-label" class="month-label"></span>
|
||||
<button id="next-month" class="icon-btn" aria-label="Mois suivant">›</button>
|
||||
<button id="next-month" class="btn btn-secondary btn-icon" aria-label="Mois suivant"><i class="ph ph-caret-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="validation-banner" class="validation-banner"></div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span class="legend-item"><i class="dot dot-present"></i>Présent</span>
|
||||
<span class="legend-item"><i class="dot dot-absent"></i>Non-présent</span>
|
||||
<span class="legend-item"><i class="dot dot-conge"></i>Congé</span>
|
||||
<span class="legend-item"><i class="dot dot-rtt"></i>RTT</span>
|
||||
<span class="legend-item"><i class="dot dot-empty"></i>Non renseigné</span>
|
||||
<div class="lockbar">
|
||||
<i id="lock-icon" class="ph ph-pencil-simple-line lockbar-icon"></i>
|
||||
<span class="lockbar-text">
|
||||
<span id="lock-title" class="lockbar-title"></span>
|
||||
<span id="lock-detail" class="lockbar-detail"></span>
|
||||
</span>
|
||||
<span class="lockbar-actions">
|
||||
<button id="reopen-month-btn" class="btn btn-secondary" hidden>Réouvrir le mois</button>
|
||||
<button id="validate-month-btn" class="btn btn-primary" hidden>Valider mon mois</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="month-summary" class="month-summary"></div>
|
||||
<div class="toolrow">
|
||||
<span class="toolrow-label">Saisie rapide</span>
|
||||
<div id="brush-options" class="toolrow-items"></div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-weekdays">
|
||||
<div class="toolrow">
|
||||
<div id="view-options" class="toolrow-items"></div>
|
||||
<span class="toolrow-right">
|
||||
<button id="fill-workdays-btn" class="btn btn-secondary">Remplir les jours ouvrés</button>
|
||||
<button id="clear-month-btn" class="btn btn-secondary">Tout effacer</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="brush-hint" class="brush-hint"></div>
|
||||
|
||||
<div id="planning-view" class="pv-grid">
|
||||
<div class="cal-head">
|
||||
<span>Lun</span><span>Mar</span><span>Mer</span><span>Jeu</span><span>Ven</span><span>Sam</span><span>Dim</span>
|
||||
</div>
|
||||
<div id="calendar-grid" class="calendar-grid"></div>
|
||||
|
||||
<div id="calendar-actions" class="calendar-actions">
|
||||
<button id="fill-workdays-btn" class="btn btn-outline">Remplir les jours ouvrés en « Présent »</button>
|
||||
<button id="validate-month-btn" class="btn btn-primary">Valider mon mois</button>
|
||||
<div id="cal-grid" class="cal-grid"></div>
|
||||
</div>
|
||||
|
||||
<div id="planning-summary" class="summary-row"></div>
|
||||
</section>
|
||||
|
||||
<!-- COMPANIES VIEW (admin) -->
|
||||
<section id="view-companies" class="view" hidden>
|
||||
<div class="view-header">
|
||||
<h1>Sociétés</h1>
|
||||
<button id="add-company-btn" class="btn btn-primary">+ Nouvelle société</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Nom</th><th>Cadres actifs</th><th>Créée le</th><th></th></tr></thead>
|
||||
<tbody id="companies-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- USERS VIEW (admin) -->
|
||||
<section id="view-users" class="view" hidden>
|
||||
<div class="view-header">
|
||||
<h1>Utilisateurs</h1>
|
||||
<button id="add-user-btn" class="btn btn-primary">+ Nouvel utilisateur</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="filter-row">
|
||||
<label>Société :
|
||||
<select id="users-filter-company"><option value="">Toutes</option></select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Nom</th><th>E-mail</th><th>Rôle</th><th>Société</th><th>Statut</th><th></th></tr></thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PLANNINGS VIEW (admin browses any cadre calendar) -->
|
||||
<section id="view-plannings" class="view" hidden>
|
||||
<div class="view-header"><h1>Plannings</h1></div>
|
||||
<div class="card selector-card">
|
||||
<label>Société :
|
||||
<select id="plannings-company-select"></select>
|
||||
</label>
|
||||
<label>Cadre :
|
||||
<select id="plannings-cadre-select"></select>
|
||||
</label>
|
||||
<button id="plannings-view-btn" class="btn btn-primary">Afficher le planning</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VALIDATION & EXPORT VIEW (admin) -->
|
||||
<section id="view-validation" class="view" hidden>
|
||||
<div class="view-header"><h1>Validation & export mensuel</h1></div>
|
||||
<div class="card selector-card">
|
||||
<label>Société :
|
||||
<select id="validation-company-select"></select>
|
||||
</label>
|
||||
<label>Mois :
|
||||
<select id="validation-month-select"></select>
|
||||
</label>
|
||||
<label>Année :
|
||||
<select id="validation-year-select"></select>
|
||||
</label>
|
||||
<button id="validation-load-btn" class="btn btn-primary">Afficher</button>
|
||||
</div>
|
||||
|
||||
<div id="validation-summary-card" class="card" hidden>
|
||||
<div class="validation-summary-header">
|
||||
<!-- ─── PLANNINGS (admin) ─── -->
|
||||
<section id="view-plannings" class="view stack-14" hidden>
|
||||
<div>
|
||||
<h2 id="validation-company-title"></h2>
|
||||
<p id="validation-status-text" class="muted"></p>
|
||||
<h3>Plannings des cadres</h3>
|
||||
<p id="plannings-subtitle" class="lede"></p>
|
||||
</div>
|
||||
<div class="validation-summary-actions">
|
||||
<div class="filter-bar">
|
||||
<span class="field">
|
||||
<label for="pl-co">Société</label>
|
||||
<select class="input" id="pl-co"><option value="all">Toutes les sociétés</option></select>
|
||||
</span>
|
||||
<span class="field">
|
||||
<label for="pl-q">Rechercher un cadre</label>
|
||||
<input class="input" id="pl-q" placeholder="Nom ou e-mail" />
|
||||
</span>
|
||||
</div>
|
||||
<div id="cadre-rows" class="stack-6"></div>
|
||||
</section>
|
||||
|
||||
<!-- ─── VALIDATION & EXPORT (admin) ─── -->
|
||||
<section id="view-validation" class="view stack-14" hidden>
|
||||
<div>
|
||||
<h3>Validation & export</h3>
|
||||
<p class="lede">Une société se valide quand tous ses cadres ont validé leur mois.</p>
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<span class="field">
|
||||
<label for="va-co">Société</label>
|
||||
<select class="input" id="va-co"></select>
|
||||
</span>
|
||||
<span class="field">
|
||||
<label for="va-mo">Mois</label>
|
||||
<select class="input" id="va-mo"></select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card validation-card">
|
||||
<div class="validation-head">
|
||||
<span class="validation-head-text">
|
||||
<span id="validation-company-name" class="validation-company"></span>
|
||||
<span id="validation-status-text" class="validation-status"></span>
|
||||
</span>
|
||||
<span class="validation-head-actions">
|
||||
<button id="export-pdf-btn" class="btn btn-secondary"><i class="ph ph-file-pdf"></i>PDF</button>
|
||||
<button id="export-excel-btn" class="btn btn-secondary"><i class="ph ph-file-xls"></i>Excel</button>
|
||||
<button id="validate-company-btn" class="btn btn-primary">Valider la société</button>
|
||||
<button id="reopen-company-btn" class="btn btn-outline">Réouvrir</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Cadre</th><th>Statut</th><th>Validé le</th><th></th></tr></thead>
|
||||
<tbody id="validation-cadres-tbody"></tbody>
|
||||
<div class="progress"><span id="validation-progress" class="progress-bar"></span></div>
|
||||
<table class="table">
|
||||
<thead><tr><th>Cadre</th><th>Demi-journées</th><th>Statut</th><th></th></tr></thead>
|
||||
<tbody id="validation-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="export-row">
|
||||
<span>Export :</span>
|
||||
<button id="export-pdf-btn" class="btn btn-outline">Télécharger PDF</button>
|
||||
<button id="export-excel-btn" class="btn btn-outline">Télécharger Excel</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── COMPANIES (admin) ─── -->
|
||||
<section id="view-companies" class="view stack-14" hidden>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h3>Sociétés</h3>
|
||||
<p id="companies-count" class="lede"></p>
|
||||
</div>
|
||||
<button id="add-company-btn" class="btn btn-primary"><i class="ph ph-plus"></i>Nouvelle société</button>
|
||||
</div>
|
||||
<div class="card table-card">
|
||||
<table class="table">
|
||||
<thead><tr><th>Nom</th><th>Cadres actifs</th><th>Créée le</th><th></th></tr></thead>
|
||||
<tbody id="companies-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── USERS (admin) ─── -->
|
||||
<section id="view-users" class="view stack-14" hidden>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h3>Utilisateurs</h3>
|
||||
<p class="lede">Aucune inscription publique — les comptes sont créés ici.</p>
|
||||
</div>
|
||||
<button id="add-user-btn" class="btn btn-primary"><i class="ph ph-plus"></i>Nouvel utilisateur</button>
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<span class="field">
|
||||
<label for="us-co">Société</label>
|
||||
<select class="input" id="us-co"><option value="all">Toutes les sociétés</option></select>
|
||||
</span>
|
||||
<span class="field">
|
||||
<label for="us-q">Rechercher</label>
|
||||
<input class="input" id="us-q" placeholder="Nom ou e-mail" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="card table-card">
|
||||
<table class="table">
|
||||
<thead><tr><th>Nom</th><th>E-mail</th><th>Rôle</th><th>Société</th><th>Statut</th><th></th></tr></thead>
|
||||
<tbody id="users-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── HISTORY ─── -->
|
||||
<section id="view-history" class="view stack-14" hidden>
|
||||
<div>
|
||||
<h3>Historique</h3>
|
||||
<p class="lede">Vos mois saisis et leurs totaux.</p>
|
||||
</div>
|
||||
<div class="card table-card">
|
||||
<table class="table">
|
||||
<thead><tr><th>Mois</th><th>Présent</th><th>Non-présent</th><th>Congé</th><th>RTT</th><th>Statut</th><th></th></tr></thead>
|
||||
<tbody id="history-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<nav id="mobile-nav" class="mobile-only mobile-nav"></nav>
|
||||
</div>
|
||||
|
||||
<!-- STATUS POPOVER (day half-cell picker) -->
|
||||
<div id="status-popover" class="popover" hidden>
|
||||
<div class="popover-title" id="popover-title"></div>
|
||||
<button class="popover-option" data-status="present"><i class="dot dot-present"></i>Présent</button>
|
||||
<button class="popover-option" data-status="absent"><i class="dot dot-absent"></i>Non-présent</button>
|
||||
<button class="popover-option" data-status="conge"><i class="dot dot-conge"></i>Congé</button>
|
||||
<button class="popover-option" data-status="rtt"><i class="dot dot-rtt"></i>RTT</button>
|
||||
<button class="popover-option popover-clear" data-status="">Effacer</button>
|
||||
<!-- ══════════════ POPOVER ══════════════ -->
|
||||
<div id="pop-backdrop" class="pop-backdrop" hidden></div>
|
||||
<div id="pop" class="pop" hidden>
|
||||
<div class="pop-head">
|
||||
<span id="pop-title" class="pop-title"></span>
|
||||
<span class="pop-hint">1–4 au clavier</span>
|
||||
</div>
|
||||
<div id="pop-options"></div>
|
||||
<button id="pop-clear" class="pop-opt pop-opt-muted"><i class="dot st-none"></i>Effacer<span class="kbd">⌫</span></button>
|
||||
<button id="pop-whole-day" class="pop-opt"><i class="ph ph-arrows-out-line-horizontal pop-ico"></i>Appliquer à la journée<span class="kbd">↵</span></button>
|
||||
</div>
|
||||
|
||||
<!-- GENERIC MODAL -->
|
||||
<div id="modal-backdrop" class="modal-backdrop" hidden>
|
||||
<div class="modal" id="modal-box"></div>
|
||||
<!-- ══════════════ DIALOG ══════════════ -->
|
||||
<div id="dialog-backdrop" class="dialog-backdrop" hidden>
|
||||
<div id="dialog" class="dialog"></div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════ TOAST ══════════════ -->
|
||||
<div id="toast" class="toast" hidden></div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+883
-643
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user