Compare commits
10 Commits
7495e61b51
...
8061887176
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8061887176 | ||
|
|
aa095eae08 | ||
|
|
f0737a290e | ||
|
|
60fd2b891e | ||
|
|
eb1684f04c | ||
|
|
0e1cab9f6c | ||
|
|
cfeb5196db | ||
|
|
841ff1bd09 | ||
|
|
14a660892a | ||
|
|
7d4ac0721d |
29
.env.recette
Normal file
29
.env.recette
Normal file
@@ -0,0 +1,29 @@
|
||||
# Environnement de recette — itinova-budget-si
|
||||
NODE_ENV=production
|
||||
|
||||
# Base de données locale MySQL
|
||||
MYSQL_ROOT_PASSWORD=BudgetSI2027!
|
||||
MYSQL_DATABASE=budget_si
|
||||
MYSQL_USER=budget_user
|
||||
MYSQL_PASSWORD=BudgetSI2027!
|
||||
DATABASE_URL=mysql://budget_user:BudgetSI2027!@db:3306/budget_si
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=budget-si-recette-jwt-secret-2027
|
||||
|
||||
# OAuth Manus (désactivé en recette — auth locale uniquement)
|
||||
VITE_APP_ID=
|
||||
OAUTH_SERVER_URL=
|
||||
VITE_OAUTH_PORTAL_URL=
|
||||
OWNER_OPEN_ID=
|
||||
OWNER_NAME=
|
||||
|
||||
# Forge API (désactivé en recette)
|
||||
BUILT_IN_FORGE_API_URL=
|
||||
BUILT_IN_FORGE_API_KEY=
|
||||
VITE_FRONTEND_FORGE_API_KEY=
|
||||
VITE_FRONTEND_FORGE_API_URL=
|
||||
|
||||
# Analytics (désactivé en recette)
|
||||
VITE_ANALYTICS_ENDPOINT=
|
||||
VITE_ANALYTICS_WEBSITE_ID=
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copier les fichiers de dépendances
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm && pnpm install --frozen-lockfile
|
||||
|
||||
# Copier le reste du code
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN pnpm build
|
||||
|
||||
# ── Image de production ──────────────────────────────────────────────────────
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copier les dépendances de production uniquement
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
|
||||
# Copier le build
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Copier les fichiers de configuration nécessaires au runtime
|
||||
COPY drizzle ./drizzle
|
||||
COPY drizzle.config.ts ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
Pencil,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Key,
|
||||
Upload,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
@@ -373,9 +376,439 @@ function LigneForm({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Composant Clés de répartition ──────────────────────────────────────────
|
||||
|
||||
interface BaseRow {
|
||||
annee: number;
|
||||
etablissementCode: string;
|
||||
etablissementNom: string | null;
|
||||
baseRepartition: string;
|
||||
baseRepartitionHep: string | null;
|
||||
modeManuel: boolean;
|
||||
}
|
||||
|
||||
interface ClesRepartitionViewProps {
|
||||
annee: number;
|
||||
basesRepartitionRaw: BaseRow[];
|
||||
isLoadingBases: boolean;
|
||||
isErrorBases: boolean;
|
||||
setBaseRepartition: ReturnType<typeof trpc.opex.setBaseRepartition.useMutation>;
|
||||
importBasesRepartition: ReturnType<typeof trpc.opex.importBasesRepartition.useMutation>;
|
||||
batchSetMontantsEtab: ReturnType<typeof trpc.opex.batchSetMontantsEtab.useMutation>;
|
||||
etablissementsRecalcules: Array<{ code: string; nom: string; montants: Record<string, number>; total: number }>;
|
||||
}
|
||||
|
||||
function ClesRepartitionView({ annee, basesRepartitionRaw, isLoadingBases, isErrorBases, setBaseRepartition, importBasesRepartition, batchSetMontantsEtab, etablissementsRecalcules }: ClesRepartitionViewProps) {
|
||||
const anneeBase = annee - 2; // La base de répartition correspond à l'année OPEX - 2
|
||||
const [editingCell, setEditingCell] = useState<{ code: string; col: 'base' | 'hep' } | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [searchBase, setSearchBase] = useState('');
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return basesRepartitionRaw
|
||||
.filter(r => {
|
||||
if (!searchBase) return true;
|
||||
const s = searchBase.toLowerCase();
|
||||
return r.etablissementCode.toLowerCase().includes(s) || (r.etablissementNom ?? '').toLowerCase().includes(s);
|
||||
})
|
||||
.sort((a, b) => a.etablissementCode.localeCompare(b.etablissementCode));
|
||||
}, [basesRepartitionRaw, searchBase]);
|
||||
|
||||
const totalBase = useMemo(() => basesRepartitionRaw.reduce((s, r) => s + parseFloat(r.baseRepartition ?? '0'), 0), [basesRepartitionRaw]);
|
||||
const totalHep = useMemo(() => basesRepartitionRaw.reduce((s, r) => s + parseFloat(r.baseRepartitionHep ?? '0'), 0), [basesRepartitionRaw]);
|
||||
|
||||
function startEdit(code: string, col: 'base' | 'hep', currentVal: string) {
|
||||
setEditingCell({ code, col });
|
||||
setEditValue(currentVal.replace(/[^0-9.]/g, ''));
|
||||
}
|
||||
|
||||
function commitEdit(row: BaseRow) {
|
||||
if (!editingCell) return;
|
||||
const numVal = parseFloat(editValue.replace(',', '.')) || 0;
|
||||
const baseRepartition = editingCell.col === 'base' ? numVal : parseFloat(row.baseRepartition ?? '0');
|
||||
const baseRepartitionHep = editingCell.col === 'hep' ? numVal : parseFloat(row.baseRepartitionHep ?? '0');
|
||||
setBaseRepartition.mutate({
|
||||
annee,
|
||||
etablissementCode: row.etablissementCode,
|
||||
etablissementNom: row.etablissementNom,
|
||||
baseRepartition,
|
||||
baseRepartitionHep,
|
||||
modeManuel: row.modeManuel,
|
||||
});
|
||||
setEditingCell(null);
|
||||
}
|
||||
|
||||
function toggleModeManuel(row: BaseRow) {
|
||||
const activating = !row.modeManuel;
|
||||
if (activating) {
|
||||
// Avant d'activer le mode manuel, on pré-remplit les montants avec les valeurs
|
||||
// calculées automatiquement (prorata) pour cet établissement
|
||||
const etabCalc = etablissementsRecalcules.find(e => e.code === row.etablissementCode);
|
||||
if (etabCalc && Object.keys(etabCalc.montants).length > 0) {
|
||||
const montantsASeeder = Object.entries(etabCalc.montants)
|
||||
.filter(([, v]) => v > 0)
|
||||
.map(([libellePoste, v]) => ({ libellePoste, montant: String(v) }));
|
||||
if (montantsASeeder.length > 0) {
|
||||
batchSetMontantsEtab.mutate(
|
||||
{ annee, etablissementCode: row.etablissementCode, montants: montantsASeeder },
|
||||
{
|
||||
onSettled: () => {
|
||||
// Activer le flag modeManuel après la sauvegarde des montants
|
||||
setBaseRepartition.mutate({
|
||||
annee,
|
||||
etablissementCode: row.etablissementCode,
|
||||
etablissementNom: row.etablissementNom,
|
||||
baseRepartition: parseFloat(row.baseRepartition ?? '0'),
|
||||
baseRepartitionHep: parseFloat(row.baseRepartitionHep ?? '0'),
|
||||
modeManuel: true,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cas normal : désactivation ou activation sans montants à seeder
|
||||
setBaseRepartition.mutate({
|
||||
annee,
|
||||
etablissementCode: row.etablissementCode,
|
||||
etablissementNom: row.etablissementNom,
|
||||
baseRepartition: parseFloat(row.baseRepartition ?? '0'),
|
||||
baseRepartitionHep: parseFloat(row.baseRepartitionHep ?? '0'),
|
||||
modeManuel: activating,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFileImport(file: File) {
|
||||
try {
|
||||
const XLSX = await import('xlsx');
|
||||
const buf = await file.arrayBuffer();
|
||||
const wb = XLSX.read(buf, { type: 'array' });
|
||||
|
||||
// ── Sélection intelligente de la feuille ──────────────────────────────
|
||||
// Priorité : feuille dont le nom contient OPEX, DSI, répartition ou base
|
||||
// (insensible à la casse et aux accents). Sinon, première feuille.
|
||||
const SHEET_KEYWORDS = ['opex', 'dsi', 'repartition', 'répartition', 'base'];
|
||||
const selectedSheetName =
|
||||
wb.SheetNames.find(name => {
|
||||
const lower = name.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
return SHEET_KEYWORDS.some(kw => lower.includes(kw));
|
||||
}) ?? wb.SheetNames[0];
|
||||
|
||||
const ws = wb.Sheets[selectedSheetName];
|
||||
|
||||
// Informer l'utilisateur de la feuille utilisée si le classeur en contient plusieurs
|
||||
if (wb.SheetNames.length > 1) {
|
||||
toast.info(`Feuille utilisée : « ${selectedSheetName} »`, {
|
||||
description: `${wb.SheetNames.length} feuilles disponibles dans le classeur.`,
|
||||
});
|
||||
}
|
||||
|
||||
const data: string[][] = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' }) as string[][];
|
||||
|
||||
// ── Détection de la ligne d'en-tête ──────────────────────────────────
|
||||
// Cherche dans les 15 premières lignes une ligne contenant un code établissement
|
||||
let headerIdx = -1;
|
||||
for (let i = 0; i < Math.min(data.length, 15); i++) {
|
||||
const row = data[i].map(c => String(c).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''));
|
||||
if (row.some(c =>
|
||||
c.includes('code') ||
|
||||
c.includes('etablissement') ||
|
||||
c.includes('etab') ||
|
||||
c.includes('structure')
|
||||
)) {
|
||||
headerIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (headerIdx === -1) {
|
||||
toast.error('Format non reconnu', {
|
||||
description: `Impossible de trouver la ligne d'en-tête dans la feuille « ${selectedSheetName} ». Colonnes attendues : code établissement, base de répartition.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Détection des colonnes ────────────────────────────────────────────
|
||||
const headers = data[headerIdx].map(c =>
|
||||
String(c).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
);
|
||||
const codeIdx = headers.findIndex(h => h.includes('code'));
|
||||
const nomIdx = headers.findIndex(h =>
|
||||
h.includes('nom') || h.includes('etablissement') || h.includes('etab') || h.includes('libelle')
|
||||
);
|
||||
// Colonne base standard : contient 'base' ou 'repartition' mais PAS 'hep'
|
||||
const baseIdx = headers.findIndex(h =>
|
||||
(h.includes('base') || h.includes('repartition')) && !h.includes('hep')
|
||||
);
|
||||
// Colonne base HEP : contient 'hep'
|
||||
const hepIdx = headers.findIndex(h => h.includes('hep'));
|
||||
|
||||
if (codeIdx === -1 || baseIdx === -1) {
|
||||
toast.error('Colonnes manquantes', {
|
||||
description: `Le fichier doit contenir au minimum les colonnes : code établissement, base de répartition. Colonnes détectées : ${headers.filter(Boolean).join(', ') || '(aucune)'}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Construction des lignes à importer ───────────────────────────────
|
||||
// Mots-clés qui indiquent une ligne parasite (catégorie ou synthèse, pas un établissement)
|
||||
const PARASITE_PATTERNS = /^(infog|s.curit|t.l.phonie|app global|app hep|app smr|applicatifs|total|ventilation|r.partition|nouveaut)/i;
|
||||
const importRows = data.slice(headerIdx + 1)
|
||||
.filter(row => {
|
||||
const code = String(row[codeIdx] ?? '').trim();
|
||||
if (!code) return false;
|
||||
// Rejeter les lignes dont le code ressemble à un libellé de catégorie
|
||||
const codeNorm = code.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
if (PARASITE_PATTERNS.test(codeNorm)) return false;
|
||||
return true;
|
||||
})
|
||||
.map(row => ({
|
||||
etablissementCode: String(row[codeIdx]).trim(),
|
||||
etablissementNom: nomIdx >= 0 ? String(row[nomIdx]).trim() || null : null,
|
||||
baseRepartition: parseFloat(String(row[baseIdx]).replace(/[^0-9.,]/g, '').replace(',', '.')) || 0,
|
||||
baseRepartitionHep: hepIdx >= 0
|
||||
? parseFloat(String(row[hepIdx]).replace(/[^0-9.,]/g, '').replace(',', '.')) || 0
|
||||
: 0,
|
||||
}));
|
||||
|
||||
if (importRows.length === 0) {
|
||||
toast.error('Aucune ligne importée', {
|
||||
description: 'Le fichier ne contient aucune ligne de données valide après la ligne d\'en-tête.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
importBasesRepartition.mutate({ annee, rows: importRows });
|
||||
} catch (e) {
|
||||
toast.error('Erreur lecture fichier', { description: String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
function formatBaseNum(val: string | null | undefined): string {
|
||||
const n = parseFloat(val ?? '0');
|
||||
if (!n) return '—';
|
||||
return new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(n) + ' €';
|
||||
}
|
||||
|
||||
if (isLoadingBases) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-3 text-muted-foreground">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
<p className="text-sm">Chargement des bases de répartition…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorBases) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-3">
|
||||
<AlertCircle className="w-8 h-8 text-destructive" />
|
||||
<p className="text-sm font-semibold text-destructive">Erreur lors du chargement des bases de répartition</p>
|
||||
<p className="text-xs text-muted-foreground">Vérifiez votre connexion et rechargez la page.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* Bandeau d'information */}
|
||||
<div className="flex-shrink-0 mb-4 p-4 rounded-xl bg-blue-50 border border-blue-200 flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-semibold mb-1">Base de répartition {anneeBase} — OPEX {annee}</p>
|
||||
<p>La base de répartition utilisée pour l'OPEX <strong>{annee}</strong> correspond aux charges de classe 6 de l'exercice <strong>{anneeBase}</strong> (année OPEX − 2).</p>
|
||||
<p className="mt-1">La colonne <strong>Base / HEP</strong> est utilisée exclusivement pour le poste <em>DUI pôle HEP</em>. Pour les établissements hors pôle HEP, cette valeur doit être 0.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Barre d'outils */}
|
||||
<div className="flex-shrink-0 flex items-center gap-3 mb-3 flex-wrap">
|
||||
<div className="relative flex-1 min-w-48 max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher un établissement..."
|
||||
value={searchBase}
|
||||
onChange={e => setSearchBase(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 text-sm bg-muted/50 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{basesRepartitionRaw.length}</span> établissements
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
className="hidden"
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) handleFileImport(f); e.target.value = ''; }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={importBasesRepartition.isPending}
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg border border-border bg-card hover:bg-muted/50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{importBasesRepartition.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
|
||||
Importer fichier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Zone de drop */}
|
||||
<div
|
||||
className={`flex-shrink-0 mb-3 border-2 border-dashed rounded-xl p-3 text-center text-xs text-muted-foreground transition-colors ${
|
||||
isDragging ? 'border-primary bg-primary/5 text-primary' : 'border-border hover:border-primary/40'
|
||||
}`}
|
||||
onDragOver={e => { e.preventDefault(); setIsDragging(true); }}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={e => { e.preventDefault(); setIsDragging(false); const f = e.dataTransfer.files?.[0]; if (f) handleFileImport(f); }}
|
||||
>
|
||||
Glissez un fichier Excel (.xlsx) ou CSV ici pour importer les bases de répartition
|
||||
<span className="block mt-0.5 text-muted-foreground/60">Colonnes attendues : code établissement · base de répartition · base répartition HEP (optionnel)</span>
|
||||
</div>
|
||||
|
||||
{/* Tableau */}
|
||||
<div className="flex-1 overflow-auto rounded-xl border border-border">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-muted/40 border-b border-border sticky top-0 z-10">
|
||||
<th className="px-4 py-3 text-left font-semibold text-foreground text-xs uppercase tracking-wide w-28">Code</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-foreground text-xs uppercase tracking-wide">Établissement</th>
|
||||
<th className="px-4 py-3 text-right font-semibold text-foreground text-xs uppercase tracking-wide w-48">
|
||||
Base de répartition {anneeBase}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right font-semibold text-foreground text-xs uppercase tracking-wide w-48">
|
||||
Base répartition {anneeBase} / HEP
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-semibold text-foreground text-xs uppercase tracking-wide w-36" title="Si activé, tous les montants OPEX de cet établissement sont saisis manuellement (pas de calcul prorata)">
|
||||
Tout manuel
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-12 text-center text-muted-foreground">
|
||||
{basesRepartitionRaw.length === 0
|
||||
? `Aucune base de répartition pour ${annee}. Importez un fichier ou saisissez les valeurs manuellement.`
|
||||
: 'Aucun résultat pour cette recherche.'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{rows.map((row, idx) => (
|
||||
<tr key={row.etablissementCode} className={`border-b border-border/50 hover:bg-muted/20 transition-colors ${row.modeManuel ? 'bg-amber-50/60' : idx % 2 === 0 ? '' : 'bg-muted/10'}`}>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground">{row.etablissementCode}</td>
|
||||
<td className="px-4 py-2.5 font-medium text-foreground">
|
||||
{row.etablissementNom ?? row.etablissementCode}
|
||||
{row.modeManuel && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-amber-100 text-amber-700 border border-amber-200">
|
||||
Manuel
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{/* Colonne Base de répartition */}
|
||||
<td className="px-4 py-2.5 text-right">
|
||||
{editingCell?.code === row.etablissementCode && editingCell.col === 'base' ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => commitEdit(row)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') commitEdit(row); if (e.key === 'Escape') setEditingCell(null); }}
|
||||
className="w-full text-right px-2 py-1 border border-primary rounded text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 tabular-nums"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => startEdit(row.etablissementCode, 'base', row.baseRepartition)}
|
||||
className="w-full text-right px-2 py-1 rounded hover:bg-primary/10 transition-colors tabular-nums font-medium text-foreground group"
|
||||
>
|
||||
{formatBaseNum(row.baseRepartition)}
|
||||
<Pencil className="inline-block ml-1 w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
{/* Colonne Base / HEP */}
|
||||
<td className="px-4 py-2.5 text-right">
|
||||
{editingCell?.code === row.etablissementCode && editingCell.col === 'hep' ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => commitEdit(row)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') commitEdit(row); if (e.key === 'Escape') setEditingCell(null); }}
|
||||
className="w-full text-right px-2 py-1 border border-primary rounded text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 tabular-nums"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => startEdit(row.etablissementCode, 'hep', row.baseRepartitionHep ?? '0')}
|
||||
className={`w-full text-right px-2 py-1 rounded hover:bg-primary/10 transition-colors tabular-nums group ${
|
||||
parseFloat(row.baseRepartitionHep ?? '0') > 0 ? 'font-medium text-orange-600' : 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{parseFloat(row.baseRepartitionHep ?? '0') > 0 ? formatBaseNum(row.baseRepartitionHep) : '—'}
|
||||
<Pencil className="inline-block ml-1 w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
{/* Colonne Mode Manuel */}
|
||||
<td className="px-4 py-2.5 text-center">
|
||||
<button
|
||||
onClick={() => toggleModeManuel(row)}
|
||||
disabled={setBaseRepartition.isPending}
|
||||
title={row.modeManuel
|
||||
? 'Mode manuel activé — cliquer pour revenir au calcul prorata automatique'
|
||||
: 'Cliquer pour activer le mode tout manuel (montants OPEX saisis manuellement pour cet établissement)'}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 ${
|
||||
row.modeManuel ? 'bg-amber-500' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform ${
|
||||
row.modeManuel ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="bg-muted/40 border-t-2 border-border font-bold">
|
||||
<td colSpan={2} className="px-4 py-3 text-sm font-semibold text-foreground">TOTAL — {basesRepartitionRaw.length} établissements
|
||||
{basesRepartitionRaw.filter(r => r.modeManuel).length > 0 && (
|
||||
<span className="ml-2 text-xs font-normal text-amber-600">
|
||||
({basesRepartitionRaw.filter(r => r.modeManuel).length} en mode manuel)
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-bold text-foreground tabular-nums">
|
||||
{new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalBase)} €
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-bold text-orange-600 tabular-nums">
|
||||
{totalHep > 0 ? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalHep) + ' €' : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-xs text-muted-foreground">
|
||||
{basesRepartitionRaw.filter(r => r.modeManuel).length > 0
|
||||
? `${basesRepartitionRaw.filter(r => r.modeManuel).length} actif${basesRepartitionRaw.filter(r => r.modeManuel).length > 1 ? 's' : ''}`
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Composant principal ──────────────────────────────────────────────────────
|
||||
|
||||
type ViewMode = 'postes' | 'etablissements';
|
||||
type ViewMode = 'postes' | 'etablissements' | 'cles_repartition';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
export default function DsiOpex() {
|
||||
@@ -386,6 +819,8 @@ export default function DsiOpex() {
|
||||
const { data: postesRaw, isLoading: loadingPostes } = trpc.opex.getPostes.useQuery({ annee });
|
||||
const { data: montantsEtabRaw, isLoading: loadingMontants } = trpc.opex.getMontantsEtab.useQuery({ annee });
|
||||
const { data: validatedRow } = trpc.opex.getValidated.useQuery({ annee });
|
||||
// Bases de répartition depuis la BDD (charges classe 6 par établissement)
|
||||
const { data: basesRepartitionRaw, isLoading: isLoadingBases, isError: isErrorBases } = trpc.opex.getBasesRepartition.useQuery({ annee });
|
||||
|
||||
// ── Mutations tRPC ─────────────────────────────────────────────────────────
|
||||
const upsertPoste = trpc.opex.upsertPoste.useMutation({
|
||||
@@ -408,6 +843,18 @@ export default function DsiOpex() {
|
||||
onSuccess: () => { utils.opex.getValidated.invalidate({ annee }); toast.success(`OPEX ${annee} validé`, { description: 'Le prévisionnel est maintenant verrouillé.' }); },
|
||||
onError: (err) => toast.error('Erreur validation', { description: err.message }),
|
||||
});
|
||||
const setBaseRepartition = trpc.opex.setBaseRepartition.useMutation({
|
||||
onSuccess: () => utils.opex.getBasesRepartition.invalidate({ annee }),
|
||||
onError: (err) => toast.error('Erreur sauvegarde base', { description: err.message }),
|
||||
});
|
||||
const importBasesRepartition = trpc.opex.importBasesRepartition.useMutation({
|
||||
onSuccess: (data) => { utils.opex.getBasesRepartition.invalidate({ annee }); toast.success(`${data.count} bases de répartition importées`); },
|
||||
onError: (err) => toast.error('Erreur import', { description: err.message }),
|
||||
});
|
||||
const batchSetMontantsEtab = trpc.opex.batchSetMontantsEtab.useMutation({
|
||||
onSuccess: () => utils.opex.getMontantsEtab.invalidate({ annee }),
|
||||
onError: (err) => toast.error('Erreur sauvegarde montants', { description: err.message }),
|
||||
});
|
||||
|
||||
// ── Mapper les données BDD vers le format interne ──────────────────────────
|
||||
const lignes: LigneOpex[] = useMemo(() => {
|
||||
@@ -685,24 +1132,54 @@ export default function DsiOpex() {
|
||||
);
|
||||
|
||||
// Vue établissements : recalcul dynamique depuis les postes éditables
|
||||
// Les bases de répartition viennent de la BDD (table opex_bases_repartition)
|
||||
// Fallback sur le JSON source si la BDD n'a pas de données pour cette année
|
||||
const etablissementsRecalcules = useMemo(() => {
|
||||
const srcData = getOpexDataForAnnee(annee);
|
||||
const totalBase = srcData.etablissements.reduce((s, e) => s + e.base_repartition, 0);
|
||||
if (totalBase === 0) return srcData.etablissements.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));
|
||||
// Priorité : BDD > JSON source
|
||||
const basesFromBdd = basesRepartitionRaw && basesRepartitionRaw.length > 0
|
||||
? basesRepartitionRaw.map(b => ({
|
||||
code: b.etablissementCode,
|
||||
nom: b.etablissementNom ?? b.etablissementCode,
|
||||
base_repartition: parseFloat(b.baseRepartition ?? '0') || 0,
|
||||
base_repartition_hep: parseFloat(b.baseRepartitionHep ?? '0') || 0,
|
||||
modeManuel: b.modeManuel ?? false,
|
||||
}))
|
||||
: getOpexDataForAnnee(annee).etablissements.map(e => ({
|
||||
code: e.code,
|
||||
nom: e.nom,
|
||||
base_repartition: e.base_repartition,
|
||||
base_repartition_hep: 0, // JSON source n'a pas de base HEP
|
||||
modeManuel: false,
|
||||
}));
|
||||
|
||||
return srcData.etablissements.map(etab => {
|
||||
const totalBase = basesFromBdd.reduce((s, e) => s + e.base_repartition, 0);
|
||||
const totalBaseHep = basesFromBdd.reduce((s, e) => s + e.base_repartition_hep, 0);
|
||||
if (totalBase === 0) return basesFromBdd.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));
|
||||
|
||||
return basesFromBdd.map(etab => {
|
||||
const ratio = etab.base_repartition / totalBase;
|
||||
// Ratio HEP : utiliser la base /HEP si disponible, sinon fallback sur base standard
|
||||
const ratioHep = totalBaseHep > 0 ? etab.base_repartition_hep / totalBaseHep : ratio;
|
||||
const etabOverrides: Record<string, number> = montantsManuelEtab[etab.code] ?? {};
|
||||
|
||||
const montantsRecalcules: Record<string, number> = {};
|
||||
let totalEtab = 0;
|
||||
for (const ligne of lignes) {
|
||||
const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
|
||||
// Le poste "SAAS DUI Pole HEP" utilise la base de répartition /HEP
|
||||
const isHepPoste = ligne.libelle.toLowerCase().includes('dui') &&
|
||||
(ligne.libelle.toLowerCase().includes('hep') ||
|
||||
(ligne.libelleDetail ?? '').toLowerCase().includes('hep'));
|
||||
const ratioApplique = isHepPoste ? ratioHep : ratio;
|
||||
let montant: number;
|
||||
if (isManuel && etabOverrides[ligne.libelle] !== undefined) {
|
||||
if (etab.modeManuel) {
|
||||
// Mode tout manuel : utiliser uniquement les montants saisis manuellement,
|
||||
// sans aucun calcul prorata — si pas de saisie, le montant est 0
|
||||
montant = etabOverrides[ligne.libelle] ?? 0;
|
||||
} else if (isManuel && etabOverrides[ligne.libelle] !== undefined) {
|
||||
montant = etabOverrides[ligne.libelle];
|
||||
} else if (!isManuel && ligne.montant > 0) {
|
||||
montant = Math.round(ligne.montant * ratio);
|
||||
montant = Math.round(ligne.montant * ratioApplique);
|
||||
} else {
|
||||
montant = 0;
|
||||
}
|
||||
@@ -712,7 +1189,7 @@ export default function DsiOpex() {
|
||||
|
||||
return { ...etab, montants: montantsRecalcules, total: totalEtab };
|
||||
});
|
||||
}, [lignes, montantsManuelEtab, annee]);
|
||||
}, [lignes, montantsManuelEtab, annee, basesRepartitionRaw]);
|
||||
|
||||
const filteredEtabs = useMemo(() => {
|
||||
return etablissementsRecalcules.filter(e => {
|
||||
@@ -726,6 +1203,8 @@ export default function DsiOpex() {
|
||||
const arr = [...filteredEtabs];
|
||||
if (sortCol === 'total') arr.sort((a, b) => sortDir === 'asc' ? a.total - b.total : b.total - a.total);
|
||||
else if (sortCol === 'nom') arr.sort((a, b) => sortDir === 'asc' ? a.nom.localeCompare(b.nom) : b.nom.localeCompare(a.nom));
|
||||
else if (sortCol === 'code') arr.sort((a, b) => sortDir === 'asc' ? a.code.localeCompare(b.code) : b.code.localeCompare(a.code));
|
||||
else arr.sort((a, b) => a.code.localeCompare(b.code)); // tri par défaut : code croissant
|
||||
return arr;
|
||||
}, [filteredEtabs, sortCol, sortDir]);
|
||||
|
||||
@@ -962,6 +1441,15 @@ export default function DsiOpex() {
|
||||
<Building2 className="w-3.5 h-3.5" />
|
||||
Par établissement
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setViewMode('cles_repartition'); setSortCol(null); }}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||
viewMode === 'cles_repartition' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<Key className="w-3.5 h-3.5" />
|
||||
Clés de répartition
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowDetails(d => !d)}
|
||||
@@ -1287,7 +1775,9 @@ export default function DsiOpex() {
|
||||
<table className="text-xs border-separate border-spacing-0" style={{ minWidth: 'max-content', width: '100%' }}>
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/40">
|
||||
<th className="text-left px-3 py-3 font-medium text-muted-foreground" style={{ position: 'sticky', left: 0, top: 0, zIndex: 40, background: 'oklch(0.94 0.005 80)', boxShadow: '2px 0 4px -1px rgba(0,0,0,0.15)', width: '110px', minWidth: '110px', maxWidth: '110px' }}>Code</th>
|
||||
<th className="text-left px-3 py-3 font-medium text-muted-foreground" style={{ position: 'sticky', left: 0, top: 0, zIndex: 40, background: 'oklch(0.94 0.005 80)', boxShadow: '2px 0 4px -1px rgba(0,0,0,0.15)', width: '110px', minWidth: '110px', maxWidth: '110px' }}>
|
||||
<SortBtn col="code" label="Code" />
|
||||
</th>
|
||||
<th className="text-left px-3 py-3 font-medium text-muted-foreground" style={{ position: 'sticky', left: '110px', top: 0, zIndex: 40, background: 'oklch(0.94 0.005 80)', boxShadow: '2px 0 8px -2px rgba(0,0,0,0.2)', width: '220px', minWidth: '220px', maxWidth: '220px' }}>
|
||||
<SortBtn col="nom" label="Établissement" />
|
||||
</th>
|
||||
@@ -1326,22 +1816,33 @@ export default function DsiOpex() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedEtabs.map((etab, idx) => (
|
||||
<tr key={etab.code} className={`border-b border-border/50 hover:bg-muted/20 transition-colors ${idx % 2 === 0 ? '' : 'bg-muted/5'}`}>
|
||||
<td className="px-3 py-2 whitespace-nowrap" style={{ position: 'sticky', left: 0, zIndex: 20, background: idx % 2 === 0 ? 'oklch(1 0 0)' : 'oklch(0.97 0.003 80)', boxShadow: '2px 0 4px -1px rgba(0,0,0,0.12)', width: '110px', minWidth: '110px', maxWidth: '110px' }}>
|
||||
{sortedEtabs.map((etab, idx) => {
|
||||
const rowBg = etab.modeManuel ? 'oklch(0.98 0.02 80)' : (idx % 2 === 0 ? 'oklch(1 0 0)' : 'oklch(0.97 0.003 80)');
|
||||
return (
|
||||
<tr key={etab.code} className={`border-b border-border/50 hover:bg-muted/20 transition-colors ${etab.modeManuel ? 'bg-amber-50/40' : idx % 2 === 0 ? '' : 'bg-muted/5'}`}>
|
||||
<td className="px-3 py-2 whitespace-nowrap" style={{ position: 'sticky', left: 0, zIndex: 20, background: rowBg, boxShadow: '2px 0 4px -1px rgba(0,0,0,0.12)', width: '110px', minWidth: '110px', maxWidth: '110px' }}>
|
||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-muted-foreground">{etab.code}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-foreground whitespace-nowrap overflow-hidden text-ellipsis" style={{ position: 'sticky', left: '110px', zIndex: 20, background: idx % 2 === 0 ? 'oklch(1 0 0)' : 'oklch(0.97 0.003 80)', boxShadow: '2px 0 8px -2px rgba(0,0,0,0.2)', width: '220px', minWidth: '220px', maxWidth: '220px' }} title={etab.nom}>{etab.nom}</td>
|
||||
<td className="px-3 py-2 font-medium text-foreground whitespace-nowrap overflow-hidden text-ellipsis" style={{ position: 'sticky', left: '110px', zIndex: 20, background: rowBg, boxShadow: '2px 0 8px -2px rgba(0,0,0,0.2)', width: '220px', minWidth: '220px', maxWidth: '220px' }} title={etab.nom}>
|
||||
{etab.nom}
|
||||
{etab.modeManuel && (
|
||||
<span className="ml-1.5 inline-flex items-center px-1 py-0.5 rounded text-[10px] font-semibold bg-amber-100 text-amber-700 border border-amber-200" title="Mode tout manuel activé — montants saisis manuellement, pas de calcul prorata">
|
||||
Manuel
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{lignes.map(ligne => {
|
||||
const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
|
||||
// En mode tout manuel pour cet établissement, toutes les cellules sont éditables
|
||||
const isCellEditable = etab.modeManuel || isManuel;
|
||||
const montant = etab.montants[ligne.libelle] ?? 0;
|
||||
const cellKey = `${etab.code}|${ligne.libelle}`;
|
||||
const isEditing = inlineEditKey === cellKey;
|
||||
const hasOverride = montantsManuelEtab[etab.code]?.[ligne.libelle] !== undefined;
|
||||
|
||||
if (isManuel && isEditing) {
|
||||
if (isCellEditable && isEditing) {
|
||||
return (
|
||||
<td key={ligne.id} className="px-1 py-1 border border-blue-400 bg-blue-50 min-w-[90px]">
|
||||
<td key={ligne.id} className="px-1 py-1 border border-amber-400 bg-amber-50 min-w-[90px]">
|
||||
<input
|
||||
autoFocus
|
||||
type="number"
|
||||
@@ -1355,7 +1856,7 @@ export default function DsiOpex() {
|
||||
if (e.key === 'Enter') handleInlineEditCommit(etab.code, ligne.libelle);
|
||||
if (e.key === 'Escape') handleInlineEditCancel();
|
||||
}}
|
||||
className="w-full text-right text-xs tabular-nums px-1 py-0.5 border-0 bg-transparent focus:outline-none text-blue-800 font-medium"
|
||||
className="w-full text-right text-xs tabular-nums px-1 py-0.5 border-0 bg-transparent focus:outline-none text-amber-800 font-medium"
|
||||
style={{ minWidth: '70px' }}
|
||||
/>
|
||||
</td>
|
||||
@@ -1365,20 +1866,30 @@ export default function DsiOpex() {
|
||||
return (
|
||||
<td
|
||||
key={ligne.id}
|
||||
onClick={() => isManuel && !isValidated && handleInlineEditStart(etab.code, ligne.libelle, montant)}
|
||||
onClick={() => isCellEditable && !isValidated && handleInlineEditStart(etab.code, ligne.libelle, montant)}
|
||||
className={`px-2 py-2 text-right tabular-nums transition-colors ${
|
||||
isManuel
|
||||
? `border border-blue-200 bg-blue-50/30 text-blue-800 ${
|
||||
!isValidated ? 'cursor-pointer hover:bg-blue-100/60 hover:border-blue-400' : ''
|
||||
} ${hasOverride ? 'font-semibold' : ''}`
|
||||
: 'border border-emerald-200 bg-emerald-50/20 text-emerald-800'
|
||||
etab.modeManuel
|
||||
? `border border-amber-200 bg-amber-50/30 text-amber-800 ${
|
||||
!isValidated ? 'cursor-pointer hover:bg-amber-100/60 hover:border-amber-400' : ''
|
||||
} ${hasOverride ? 'font-semibold' : 'opacity-60'}`
|
||||
: isManuel
|
||||
? `border border-blue-200 bg-blue-50/30 text-blue-800 ${
|
||||
!isValidated ? 'cursor-pointer hover:bg-blue-100/60 hover:border-blue-400' : ''
|
||||
} ${hasOverride ? 'font-semibold' : ''}`
|
||||
: 'border border-emerald-200 bg-emerald-50/20 text-emerald-800'
|
||||
}`}
|
||||
title={isManuel && !isValidated ? 'Cliquer pour saisir le montant' : undefined}
|
||||
title={
|
||||
etab.modeManuel && !isValidated
|
||||
? 'Cliquer pour saisir le montant (mode tout manuel)'
|
||||
: isManuel && !isValidated
|
||||
? 'Cliquer pour saisir le montant'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{montant > 0
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(montant) + ' €'
|
||||
: isManuel && !isValidated
|
||||
? <span className="text-blue-300 text-[10px]">cliquer</span>
|
||||
: isCellEditable && !isValidated
|
||||
? <span className={etab.modeManuel ? 'text-amber-300 text-[10px]' : 'text-blue-300 text-[10px]'}>cliquer</span>
|
||||
: <span className="text-muted-foreground">—</span>}
|
||||
</td>
|
||||
);
|
||||
@@ -1389,7 +1900,8 @@ export default function DsiOpex() {
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="bg-muted/40 border-t-2 border-border">
|
||||
@@ -1398,7 +1910,8 @@ export default function DsiOpex() {
|
||||
</td>
|
||||
{lignes.map(ligne => {
|
||||
const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
|
||||
const totalColonne = sortedEtabs.reduce((s, e) => s + (e.montants[ligne.libelle] ?? 0), 0);
|
||||
// Utiliser le montant du poste (source BDD) pour éviter les écarts d'arrondis
|
||||
const montantPoste = ligne.montant;
|
||||
return (
|
||||
<td
|
||||
key={ligne.id}
|
||||
@@ -1406,14 +1919,15 @@ export default function DsiOpex() {
|
||||
isManuel ? 'text-blue-700 bg-blue-50/40' : 'text-emerald-700 bg-emerald-50/30'
|
||||
}`}
|
||||
>
|
||||
{totalColonne > 0
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalColonne) + ' €'
|
||||
{montantPoste > 0
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(montantPoste) + ' €'
|
||||
: '—'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="px-3 py-3 text-right font-bold text-orange-600 text-sm tabular-nums" style={{ position: 'sticky', right: 0, zIndex: 20, background: 'oklch(0.94 0.005 80)', boxShadow: '-3px 0 8px -2px rgba(0,0,0,0.18)', width: '120px', minWidth: '120px' }}>
|
||||
{formatEuros(sortedEtabs.reduce((s, e) => s + e.total, 0))}
|
||||
{/* Total depuis les montants BDD — pas la somme des arrondis par établissement */}
|
||||
{formatEuros(totalGlobal)}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -1422,6 +1936,19 @@ export default function DsiOpex() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Vue Clés de répartition */}
|
||||
{viewMode === 'cles_repartition' && (
|
||||
<ClesRepartitionView
|
||||
annee={annee}
|
||||
basesRepartitionRaw={basesRepartitionRaw ?? []}
|
||||
isLoadingBases={isLoadingBases}
|
||||
isErrorBases={isErrorBases}
|
||||
setBaseRepartition={setBaseRepartition}
|
||||
importBasesRepartition={importBasesRepartition}
|
||||
batchSetMontantsEtab={batchSetMontantsEtab}
|
||||
etablissementsRecalcules={etablissementsRecalcules}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
68
docker-compose.yml
Normal file
68
docker-compose.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: itinova-budget-si-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3042:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- VITE_APP_ID=${VITE_APP_ID}
|
||||
- OAUTH_SERVER_URL=${OAUTH_SERVER_URL}
|
||||
- VITE_OAUTH_PORTAL_URL=${VITE_OAUTH_PORTAL_URL}
|
||||
- OWNER_OPEN_ID=${OWNER_OPEN_ID}
|
||||
- OWNER_NAME=${OWNER_NAME}
|
||||
- BUILT_IN_FORGE_API_URL=${BUILT_IN_FORGE_API_URL}
|
||||
- BUILT_IN_FORGE_API_KEY=${BUILT_IN_FORGE_API_KEY}
|
||||
- VITE_FRONTEND_FORGE_API_KEY=${VITE_FRONTEND_FORGE_API_KEY}
|
||||
- VITE_FRONTEND_FORGE_API_URL=${VITE_FRONTEND_FORGE_API_URL}
|
||||
- VITE_ANALYTICS_ENDPOINT=${VITE_ANALYTICS_ENDPOINT}
|
||||
- VITE_ANALYTICS_WEBSITE_ID=${VITE_ANALYTICS_WEBSITE_ID}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- budget-net
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: itinova-budget-si-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-BudgetSI2027!}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-budget_si}
|
||||
MYSQL_USER: ${MYSQL_USER:-budget_user}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-BudgetSI2027!}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-BudgetSI2027!}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- budget-net
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: itinova-budget-si-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3043:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- budget-net
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
budget-net:
|
||||
driver: bridge
|
||||
9
drizzle/0001_flaky_kitty_pryde.sql
Normal file
9
drizzle/0001_flaky_kitty_pryde.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `opex_bases_repartition` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`annee` int NOT NULL,
|
||||
`etablissementCode` varchar(50) NOT NULL,
|
||||
`etablissementNom` varchar(255),
|
||||
`baseRepartition` decimal(20,6) NOT NULL,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `opex_bases_repartition_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
1
drizzle/0002_faithful_famine.sql
Normal file
1
drizzle/0002_faithful_famine.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `opex_bases_repartition` ADD `baseRepartitionHep` decimal(20,6) DEFAULT '0';
|
||||
2
drizzle/0003_tense_havok.sql
Normal file
2
drizzle/0003_tense_havok.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `opex_bases_repartition` ADD `modeManuel` boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `opex_bases_repartition` ADD CONSTRAINT `opex_bases_repartition_annee_etab_idx` UNIQUE(`annee`,`etablissementCode`);
|
||||
864
drizzle/meta/0001_snapshot.json
Normal file
864
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,864 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "7cfbd553-42c7-4195-8927-d7db39cd57d6",
|
||||
"prevId": "973efedb-ae3f-4e9d-b1e4-fb7ab4115377",
|
||||
"tables": {
|
||||
"capex_lignes": {
|
||||
"name": "capex_lignes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cle": {
|
||||
"name": "cle",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"capex_lignes_id": {
|
||||
"name": "capex_lignes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"etablissements": {
|
||||
"name": "etablissements",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"nom": {
|
||||
"name": "nom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"groupe": {
|
||||
"name": "groupe",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ville": {
|
||||
"name": "ville",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actif": {
|
||||
"name": "actif",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"etablissements_id": {
|
||||
"name": "etablissements_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"etablissements_code_unique": {
|
||||
"name": "etablissements_code_unique",
|
||||
"columns": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inventaire_meta": {
|
||||
"name": "inventaire_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"filename": {
|
||||
"name": "filename",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateImport": {
|
||||
"name": "dateImport",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"nbEtablissements": {
|
||||
"name": "nbEtablissements",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"nbFixes": {
|
||||
"name": "nbFixes",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"nbPortables": {
|
||||
"name": "nbPortables",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inventaire_meta_id": {
|
||||
"name": "inventaire_meta_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"inventaire_meta_annee_unique": {
|
||||
"name": "inventaire_meta_annee_unique",
|
||||
"columns": [
|
||||
"annee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inventaire_postes": {
|
||||
"name": "inventaire_postes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelle": {
|
||||
"name": "libelle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"typePoste": {
|
||||
"name": "typePoste",
|
||||
"type": "enum('fixe','portable')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateRef": {
|
||||
"name": "dateRef",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ageAns": {
|
||||
"name": "ageAns",
|
||||
"type": "decimal(5,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"modele": {
|
||||
"name": "modele",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fabricant": {
|
||||
"name": "fabricant",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inventaire_postes_id": {
|
||||
"name": "inventaire_postes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_bases_repartition": {
|
||||
"name": "opex_bases_repartition",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementNom": {
|
||||
"name": "etablissementNom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"baseRepartition": {
|
||||
"name": "baseRepartition",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_bases_repartition_id": {
|
||||
"name": "opex_bases_repartition_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_montants_etab": {
|
||||
"name": "opex_montants_etab",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libellePoste": {
|
||||
"name": "libellePoste",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_montants_etab_id": {
|
||||
"name": "opex_montants_etab_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_postes": {
|
||||
"name": "opex_postes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"colIdx": {
|
||||
"name": "colIdx",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelle": {
|
||||
"name": "libelle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelleCourt": {
|
||||
"name": "libelleCourt",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelleDetail": {
|
||||
"name": "libelleDetail",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fournisseur": {
|
||||
"name": "fournisseur",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"categorie": {
|
||||
"name": "categorie",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"facturation": {
|
||||
"name": "facturation",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"modeVentilation": {
|
||||
"name": "modeVentilation",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Prorata C 6'"
|
||||
},
|
||||
"compte": {
|
||||
"name": "compte",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"detail": {
|
||||
"name": "detail",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"budgetN1": {
|
||||
"name": "budgetN1",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"isCustom": {
|
||||
"name": "isCustom",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_postes_id": {
|
||||
"name": "opex_postes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_validated": {
|
||||
"name": "opex_validated",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"validatedAt": {
|
||||
"name": "validatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"validatedBy": {
|
||||
"name": "validatedBy",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_validated_id": {
|
||||
"name": "opex_validated_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"opex_validated_annee_unique": {
|
||||
"name": "opex_validated_annee_unique",
|
||||
"columns": [
|
||||
"annee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"parametres_app": {
|
||||
"name": "parametres_app",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"cle": {
|
||||
"name": "cle",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"valeur": {
|
||||
"name": "valeur",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"parametres_app_id": {
|
||||
"name": "parametres_app_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"parametres_app_cle_unique": {
|
||||
"name": "parametres_app_cle_unique",
|
||||
"columns": [
|
||||
"cle"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user_etablissements": {
|
||||
"name": "user_etablissements",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_etablissements_id": {
|
||||
"name": "user_etablissements_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"login": {
|
||||
"name": "login",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"passwordHash": {
|
||||
"name": "passwordHash",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"firstName": {
|
||||
"name": "firstName",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastName": {
|
||||
"name": "lastName",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('admin','standard','readonly')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'standard'"
|
||||
},
|
||||
"isActive": {
|
||||
"name": "isActive",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
},
|
||||
"lastSignedIn": {
|
||||
"name": "lastSignedIn",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"users_login_unique": {
|
||||
"name": "users_login_unique",
|
||||
"columns": [
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
872
drizzle/meta/0002_snapshot.json
Normal file
872
drizzle/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,872 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "baff50d1-531e-4ba7-a82a-6f9c64aa1186",
|
||||
"prevId": "7cfbd553-42c7-4195-8927-d7db39cd57d6",
|
||||
"tables": {
|
||||
"capex_lignes": {
|
||||
"name": "capex_lignes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cle": {
|
||||
"name": "cle",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"capex_lignes_id": {
|
||||
"name": "capex_lignes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"etablissements": {
|
||||
"name": "etablissements",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"nom": {
|
||||
"name": "nom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"groupe": {
|
||||
"name": "groupe",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ville": {
|
||||
"name": "ville",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actif": {
|
||||
"name": "actif",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"etablissements_id": {
|
||||
"name": "etablissements_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"etablissements_code_unique": {
|
||||
"name": "etablissements_code_unique",
|
||||
"columns": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inventaire_meta": {
|
||||
"name": "inventaire_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"filename": {
|
||||
"name": "filename",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateImport": {
|
||||
"name": "dateImport",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"nbEtablissements": {
|
||||
"name": "nbEtablissements",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"nbFixes": {
|
||||
"name": "nbFixes",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"nbPortables": {
|
||||
"name": "nbPortables",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inventaire_meta_id": {
|
||||
"name": "inventaire_meta_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"inventaire_meta_annee_unique": {
|
||||
"name": "inventaire_meta_annee_unique",
|
||||
"columns": [
|
||||
"annee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inventaire_postes": {
|
||||
"name": "inventaire_postes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelle": {
|
||||
"name": "libelle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"typePoste": {
|
||||
"name": "typePoste",
|
||||
"type": "enum('fixe','portable')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateRef": {
|
||||
"name": "dateRef",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ageAns": {
|
||||
"name": "ageAns",
|
||||
"type": "decimal(5,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"modele": {
|
||||
"name": "modele",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fabricant": {
|
||||
"name": "fabricant",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inventaire_postes_id": {
|
||||
"name": "inventaire_postes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_bases_repartition": {
|
||||
"name": "opex_bases_repartition",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementNom": {
|
||||
"name": "etablissementNom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"baseRepartition": {
|
||||
"name": "baseRepartition",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"baseRepartitionHep": {
|
||||
"name": "baseRepartitionHep",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'0'"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_bases_repartition_id": {
|
||||
"name": "opex_bases_repartition_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_montants_etab": {
|
||||
"name": "opex_montants_etab",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libellePoste": {
|
||||
"name": "libellePoste",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_montants_etab_id": {
|
||||
"name": "opex_montants_etab_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_postes": {
|
||||
"name": "opex_postes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"colIdx": {
|
||||
"name": "colIdx",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelle": {
|
||||
"name": "libelle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelleCourt": {
|
||||
"name": "libelleCourt",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelleDetail": {
|
||||
"name": "libelleDetail",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fournisseur": {
|
||||
"name": "fournisseur",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"categorie": {
|
||||
"name": "categorie",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"facturation": {
|
||||
"name": "facturation",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"modeVentilation": {
|
||||
"name": "modeVentilation",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Prorata C 6'"
|
||||
},
|
||||
"compte": {
|
||||
"name": "compte",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"detail": {
|
||||
"name": "detail",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"budgetN1": {
|
||||
"name": "budgetN1",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"isCustom": {
|
||||
"name": "isCustom",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_postes_id": {
|
||||
"name": "opex_postes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_validated": {
|
||||
"name": "opex_validated",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"validatedAt": {
|
||||
"name": "validatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"validatedBy": {
|
||||
"name": "validatedBy",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_validated_id": {
|
||||
"name": "opex_validated_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"opex_validated_annee_unique": {
|
||||
"name": "opex_validated_annee_unique",
|
||||
"columns": [
|
||||
"annee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"parametres_app": {
|
||||
"name": "parametres_app",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"cle": {
|
||||
"name": "cle",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"valeur": {
|
||||
"name": "valeur",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"parametres_app_id": {
|
||||
"name": "parametres_app_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"parametres_app_cle_unique": {
|
||||
"name": "parametres_app_cle_unique",
|
||||
"columns": [
|
||||
"cle"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user_etablissements": {
|
||||
"name": "user_etablissements",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_etablissements_id": {
|
||||
"name": "user_etablissements_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"login": {
|
||||
"name": "login",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"passwordHash": {
|
||||
"name": "passwordHash",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"firstName": {
|
||||
"name": "firstName",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastName": {
|
||||
"name": "lastName",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('admin','standard','readonly')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'standard'"
|
||||
},
|
||||
"isActive": {
|
||||
"name": "isActive",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
},
|
||||
"lastSignedIn": {
|
||||
"name": "lastSignedIn",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"users_login_unique": {
|
||||
"name": "users_login_unique",
|
||||
"columns": [
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
889
drizzle/meta/0003_snapshot.json
Normal file
889
drizzle/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,889 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "44abeed3-e7bf-47cd-a29c-59a27bf57f1a",
|
||||
"prevId": "baff50d1-531e-4ba7-a82a-6f9c64aa1186",
|
||||
"tables": {
|
||||
"capex_lignes": {
|
||||
"name": "capex_lignes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cle": {
|
||||
"name": "cle",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"capex_lignes_id": {
|
||||
"name": "capex_lignes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"etablissements": {
|
||||
"name": "etablissements",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"nom": {
|
||||
"name": "nom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"groupe": {
|
||||
"name": "groupe",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ville": {
|
||||
"name": "ville",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actif": {
|
||||
"name": "actif",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"etablissements_id": {
|
||||
"name": "etablissements_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"etablissements_code_unique": {
|
||||
"name": "etablissements_code_unique",
|
||||
"columns": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inventaire_meta": {
|
||||
"name": "inventaire_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"filename": {
|
||||
"name": "filename",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateImport": {
|
||||
"name": "dateImport",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"nbEtablissements": {
|
||||
"name": "nbEtablissements",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"nbFixes": {
|
||||
"name": "nbFixes",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"nbPortables": {
|
||||
"name": "nbPortables",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inventaire_meta_id": {
|
||||
"name": "inventaire_meta_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"inventaire_meta_annee_unique": {
|
||||
"name": "inventaire_meta_annee_unique",
|
||||
"columns": [
|
||||
"annee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inventaire_postes": {
|
||||
"name": "inventaire_postes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelle": {
|
||||
"name": "libelle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"typePoste": {
|
||||
"name": "typePoste",
|
||||
"type": "enum('fixe','portable')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateRef": {
|
||||
"name": "dateRef",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ageAns": {
|
||||
"name": "ageAns",
|
||||
"type": "decimal(5,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"modele": {
|
||||
"name": "modele",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fabricant": {
|
||||
"name": "fabricant",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inventaire_postes_id": {
|
||||
"name": "inventaire_postes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_bases_repartition": {
|
||||
"name": "opex_bases_repartition",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementNom": {
|
||||
"name": "etablissementNom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"baseRepartition": {
|
||||
"name": "baseRepartition",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"baseRepartitionHep": {
|
||||
"name": "baseRepartitionHep",
|
||||
"type": "decimal(20,6)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'0'"
|
||||
},
|
||||
"modeManuel": {
|
||||
"name": "modeManuel",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"opex_bases_repartition_annee_etab_idx": {
|
||||
"name": "opex_bases_repartition_annee_etab_idx",
|
||||
"columns": [
|
||||
"annee",
|
||||
"etablissementCode"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_bases_repartition_id": {
|
||||
"name": "opex_bases_repartition_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_montants_etab": {
|
||||
"name": "opex_montants_etab",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libellePoste": {
|
||||
"name": "libellePoste",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_montants_etab_id": {
|
||||
"name": "opex_montants_etab_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_postes": {
|
||||
"name": "opex_postes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"colIdx": {
|
||||
"name": "colIdx",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelle": {
|
||||
"name": "libelle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelleCourt": {
|
||||
"name": "libelleCourt",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"libelleDetail": {
|
||||
"name": "libelleDetail",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fournisseur": {
|
||||
"name": "fournisseur",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"categorie": {
|
||||
"name": "categorie",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"facturation": {
|
||||
"name": "facturation",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"modeVentilation": {
|
||||
"name": "modeVentilation",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Prorata C 6'"
|
||||
},
|
||||
"compte": {
|
||||
"name": "compte",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"detail": {
|
||||
"name": "detail",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"budgetN1": {
|
||||
"name": "budgetN1",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"montant": {
|
||||
"name": "montant",
|
||||
"type": "decimal(12,2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"isCustom": {
|
||||
"name": "isCustom",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_postes_id": {
|
||||
"name": "opex_postes_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"opex_validated": {
|
||||
"name": "opex_validated",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"annee": {
|
||||
"name": "annee",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"validatedAt": {
|
||||
"name": "validatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"validatedBy": {
|
||||
"name": "validatedBy",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"opex_validated_id": {
|
||||
"name": "opex_validated_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"opex_validated_annee_unique": {
|
||||
"name": "opex_validated_annee_unique",
|
||||
"columns": [
|
||||
"annee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"parametres_app": {
|
||||
"name": "parametres_app",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"cle": {
|
||||
"name": "cle",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"valeur": {
|
||||
"name": "valeur",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"parametres_app_id": {
|
||||
"name": "parametres_app_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"parametres_app_cle_unique": {
|
||||
"name": "parametres_app_cle_unique",
|
||||
"columns": [
|
||||
"cle"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user_etablissements": {
|
||||
"name": "user_etablissements",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"etablissementCode": {
|
||||
"name": "etablissementCode",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_etablissements_id": {
|
||||
"name": "user_etablissements_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"login": {
|
||||
"name": "login",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"passwordHash": {
|
||||
"name": "passwordHash",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"firstName": {
|
||||
"name": "firstName",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastName": {
|
||||
"name": "lastName",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('admin','standard','readonly')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'standard'"
|
||||
},
|
||||
"isActive": {
|
||||
"name": "isActive",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
},
|
||||
"lastSignedIn": {
|
||||
"name": "lastSignedIn",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"users_login_unique": {
|
||||
"name": "users_login_unique",
|
||||
"columns": [
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,27 @@
|
||||
"when": 1781092850222,
|
||||
"tag": "0000_fat_falcon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1781158775157,
|
||||
"tag": "0001_flaky_kitty_pryde",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "5",
|
||||
"when": 1781161698458,
|
||||
"tag": "0002_faithful_famine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "5",
|
||||
"when": 1781163262493,
|
||||
"tag": "0003_tense_havok",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
mysqlTable,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
} from "drizzle-orm/mysql-core";
|
||||
|
||||
@@ -160,3 +161,26 @@ export const capexLignes = mysqlTable("capex_lignes", {
|
||||
|
||||
export type CapexLigne = typeof capexLignes.$inferSelect;
|
||||
export type InsertCapexLigne = typeof capexLignes.$inferInsert;
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────────
|
||||
// OPEX — Bases de répartition par établissement et par année
|
||||
// (charges de classe 6 utilisées pour le prorata)
|
||||
// ───────────────────────────────────────────────────────────────────────────────
|
||||
export const opexBasesRepartition = mysqlTable("opex_bases_repartition", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
annee: int("annee").notNull(),
|
||||
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
|
||||
etablissementNom: varchar("etablissementNom", { length: 255 }),
|
||||
/** Charges classe 6 — base de répartition standard (tous établissements) */
|
||||
baseRepartition: decimal("baseRepartition", { precision: 20, scale: 6 }).notNull(),
|
||||
/** Charges classe 6 — base de répartition pôle HEP uniquement (0 pour les étblissements hors HEP) */
|
||||
baseRepartitionHep: decimal("baseRepartitionHep", { precision: 20, scale: 6 }).default("0"),
|
||||
/** Si true, tous les montants OPEX de cet établissement sont saisis manuellement (pas de calcul prorata) */
|
||||
modeManuel: boolean("modeManuel").default(false).notNull(),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
}, (t) => ({
|
||||
uniqAnneeEtab: uniqueIndex("opex_bases_repartition_annee_etab_idx").on(t.annee, t.etablissementCode),
|
||||
}));
|
||||
|
||||
export type OpexBaseRepartition = typeof opexBasesRepartition.$inferSelect;
|
||||
export type InsertOpexBaseRepartition = typeof opexBasesRepartition.$inferInsert;
|
||||
|
||||
20
nginx.conf
Normal file
20
nginx.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 50M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
}
|
||||
51
scripts/clean-bases-parasites.mjs
Normal file
51
scripts/clean-bases-parasites.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createConnection } from 'mysql2/promise';
|
||||
import { config } from 'dotenv';
|
||||
config();
|
||||
|
||||
const conn = await createConnection(process.env.DATABASE_URL);
|
||||
|
||||
// Libellés parasites connus (noms de catégories ou lignes de synthèse)
|
||||
const PARASITES_KEYWORDS = [
|
||||
'Infogérance', 'Infogerance', 'Sécurité', 'Securite',
|
||||
'Téléphonie', 'Telephonie',
|
||||
'App global', 'App HEP', 'App SMR',
|
||||
'Applicatifs', // commence par
|
||||
'TOTAL', 'Total',
|
||||
'Ventilation', 'Répartition', 'Repartition',
|
||||
'Nouveautés', 'Nouveautes',
|
||||
];
|
||||
|
||||
// 1. Lister les lignes parasites
|
||||
const [rows] = await conn.execute(
|
||||
`SELECT id, annee, etablissementCode, etablissementNom FROM opex_bases_repartition ORDER BY annee, etablissementCode`
|
||||
);
|
||||
|
||||
console.log('=== Toutes les lignes opex_bases_repartition ===');
|
||||
for (const r of rows) {
|
||||
const code = r.etablissementCode ?? '';
|
||||
const nom = r.etablissementNom ?? '';
|
||||
const isParasite = PARASITES_KEYWORDS.some(k =>
|
||||
code.toLowerCase().includes(k.toLowerCase()) ||
|
||||
nom.toLowerCase().includes(k.toLowerCase())
|
||||
) || /^(total|ventilation|répartition|applicatifs)/i.test(code.trim());
|
||||
if (isParasite) {
|
||||
console.log(` PARASITE id=${r.id} annee=${r.annee} code="${code}" nom="${nom}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Supprimer les parasites
|
||||
const [delResult] = await conn.execute(
|
||||
`DELETE FROM opex_bases_repartition
|
||||
WHERE etablissementCode REGEXP '^(Infog|S.curit|T.l.phonie|App global|App HEP|App SMR|Applicatifs|TOTAL|Total|Ventilation|R.partition|Nouveaut)'
|
||||
OR etablissementNom REGEXP '^(Infog|S.curit|T.l.phonie|App global|App HEP|App SMR|Applicatifs|TOTAL|Total|Ventilation|R.partition|Nouveaut)'`
|
||||
);
|
||||
console.log(`\nSupprimé ${delResult.affectedRows} lignes parasites.`);
|
||||
|
||||
// 3. Vérification finale
|
||||
const [remaining] = await conn.execute(
|
||||
`SELECT annee, COUNT(*) as cnt FROM opex_bases_repartition GROUP BY annee ORDER BY annee`
|
||||
);
|
||||
console.log('=== Lignes restantes par année ===');
|
||||
console.table(remaining);
|
||||
|
||||
await conn.end();
|
||||
45
scripts/compare-opex-bdd-json.mjs
Normal file
45
scripts/compare-opex-bdd-json.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: join(__dirname, '..', '.env') });
|
||||
|
||||
const conn = await mysql.createConnection(process.env.DATABASE_URL);
|
||||
const [rows] = await conn.execute('SELECT libelle, montant FROM opex_postes WHERE annee = 2026 ORDER BY colIdx');
|
||||
|
||||
const data = JSON.parse(readFileSync(join(__dirname, '../client/src/data_opex.json'), 'utf8'));
|
||||
const libellesJson = new Set(data.postes.map(p => p.libelle));
|
||||
const libellesBdd = new Set(rows.map(r => r.libelle));
|
||||
|
||||
console.log('Nb postes BDD:', rows.length);
|
||||
console.log('Nb postes JSON:', libellesJson.size);
|
||||
|
||||
let totalBdd = 0;
|
||||
rows.forEach(r => { totalBdd += parseFloat(r.montant || '0'); });
|
||||
console.log('Total BDD:', totalBdd.toFixed(2));
|
||||
|
||||
console.log('\n=== En BDD mais PAS dans JSON ===');
|
||||
for (const lib of libellesBdd) {
|
||||
if (!libellesJson.has(lib)) {
|
||||
const row = rows.find(r => r.libelle === lib);
|
||||
console.log(' +', lib, ':', row.montant);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Dans JSON mais PAS en BDD ===');
|
||||
for (const lib of libellesJson) {
|
||||
if (!libellesBdd.has(lib)) {
|
||||
console.log(' -', lib);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Tous les postes BDD avec montant ===');
|
||||
rows.forEach(r => {
|
||||
const m = parseFloat(r.montant || '0');
|
||||
if (m > 0) console.log(` ${r.libelle}: ${m.toFixed(2)}`);
|
||||
});
|
||||
|
||||
await conn.end();
|
||||
33
scripts/dedup-opex-postes.mjs
Normal file
33
scripts/dedup-opex-postes.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createConnection } from 'mysql2/promise';
|
||||
import { config } from 'dotenv';
|
||||
config();
|
||||
|
||||
const conn = await createConnection(process.env.DATABASE_URL);
|
||||
|
||||
// 1. Voir les doublons
|
||||
const [doublons] = await conn.execute(
|
||||
'SELECT annee, libelle, COUNT(*) as cnt FROM opex_postes GROUP BY annee, libelle HAVING cnt > 1 ORDER BY cnt DESC LIMIT 30'
|
||||
);
|
||||
console.log('=== Doublons (annee, libelle) ===');
|
||||
console.table(doublons);
|
||||
|
||||
// 2. Total par année
|
||||
const [totaux] = await conn.execute(
|
||||
'SELECT annee, COUNT(*) as cnt FROM opex_postes GROUP BY annee ORDER BY annee'
|
||||
);
|
||||
console.log('=== Total postes par année ===');
|
||||
console.table(totaux);
|
||||
|
||||
// 3. Libellés qui ressemblent à des catégories (parasites)
|
||||
const [parasites] = await conn.execute(
|
||||
`SELECT id, annee, libelle, categorie FROM opex_postes
|
||||
WHERE libelle IN ('Infogérance','Sécurité','Téléphonie','App global','App HEP','App SMR',
|
||||
'Applicatifs communs','Applicatifs PA','Applicatifs HEP','Applicatifs SMR','Nouveautés','TOTAL',
|
||||
'Ventilation cout','Ventilation coût')
|
||||
OR libelle REGEXP '^(total|ventilation|répartition)'
|
||||
ORDER BY annee, libelle`
|
||||
);
|
||||
console.log('=== Libellés parasites en BDD ===');
|
||||
console.table(parasites);
|
||||
|
||||
await conn.end();
|
||||
25
scripts/list-non-standard-codes.mjs
Normal file
25
scripts/list-non-standard-codes.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createConnection } from 'mysql2/promise';
|
||||
import { config } from 'dotenv';
|
||||
config();
|
||||
|
||||
const conn = await createConnection(process.env.DATABASE_URL);
|
||||
|
||||
// Lister toutes les lignes dont le code n'est pas un code établissement standard (7 chiffres + 2 lettres)
|
||||
const [rows] = await conn.execute(
|
||||
`SELECT id, annee, etablissementCode, etablissementNom, baseRepartition
|
||||
FROM opex_bases_repartition
|
||||
ORDER BY etablissementCode`
|
||||
);
|
||||
|
||||
console.log('=== Codes non-standard (parasites potentiels) ===');
|
||||
for (const r of rows) {
|
||||
const code = r.etablissementCode ?? '';
|
||||
// Code standard = 7 chiffres + 2 lettres majuscules (ex: 1001BPT)
|
||||
const isStandard = /^\d{4,7}[A-Z]{2,3}$/.test(code);
|
||||
if (!isStandard) {
|
||||
console.log(`id=${r.id} annee=${r.annee} code="${code}" nom="${r.etablissementNom}" base=${r.baseRepartition}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal lignes: ${rows.length}`);
|
||||
await conn.end();
|
||||
106
scripts/seed-opex-bases.mjs
Normal file
106
scripts/seed-opex-bases.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Seed des bases de répartition OPEX (charges classe 6) pour 2025 et 2026
|
||||
* depuis les fichiers JSON sources.
|
||||
*
|
||||
* Inclut désormais la colonne baseRepartitionHep (pôle HEP uniquement).
|
||||
* Données 2026 extraites du fichier Excel source via extract_bases_repartition.py.
|
||||
*
|
||||
* Usage: node scripts/seed-opex-bases.mjs
|
||||
*/
|
||||
import mysql from 'mysql2/promise';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: join(__dirname, '..', '.env') });
|
||||
|
||||
const conn = await mysql.createConnection(process.env.DATABASE_URL);
|
||||
|
||||
// ── Année 2025 — depuis le JSON source (pas de colonne HEP disponible) ────────
|
||||
{
|
||||
const annee = 2025;
|
||||
const filePath = join(__dirname, '../client/src/data_opex_2025.json');
|
||||
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
const etabs = data.etablissements || [];
|
||||
|
||||
if (etabs.length === 0) {
|
||||
console.log(`Année ${annee}: aucun établissement trouvé dans le JSON`);
|
||||
} else {
|
||||
await conn.execute('DELETE FROM opex_bases_repartition WHERE annee = ?', [annee]);
|
||||
const values = etabs.map(e => [
|
||||
annee,
|
||||
e.code,
|
||||
e.nom || null,
|
||||
e.base_repartition || 0,
|
||||
0, // baseRepartitionHep non disponible pour 2025
|
||||
]);
|
||||
await conn.query(
|
||||
'INSERT INTO opex_bases_repartition (annee, etablissementCode, etablissementNom, baseRepartition, baseRepartitionHep) VALUES ?',
|
||||
[values]
|
||||
);
|
||||
console.log(`Année ${annee}: ${values.length} bases de répartition insérées (HEP: non disponible → 0)`);
|
||||
console.log(` Total base: ${etabs.reduce((s, e) => s + (e.base_repartition || 0), 0).toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Année 2026 — depuis le JSON extrait du fichier Excel source ───────────────
|
||||
// Données extraites par /home/ubuntu/extract_bases_repartition.py
|
||||
// Fichier: /home/ubuntu/bases_repartition_2026.json
|
||||
{
|
||||
const annee = 2026;
|
||||
let bases2026;
|
||||
try {
|
||||
bases2026 = JSON.parse(readFileSync('/home/ubuntu/bases_repartition_2026.json', 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Fichier bases_repartition_2026.json non trouvé. Fallback sur le JSON source.');
|
||||
const filePath = join(__dirname, '../client/src/data_opex.json');
|
||||
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
bases2026 = (data.etablissements || []).map(e => ({
|
||||
code: e.code,
|
||||
nom: e.nom,
|
||||
base_repartition: e.base_repartition || 0,
|
||||
base_repartition_hep: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
// Filtrer les lignes parasites (totaux, applicatifs, etc.)
|
||||
const etablissements = bases2026.filter(e => {
|
||||
const code = String(e.code).trim();
|
||||
const nom = String(e.nom || '').trim().toLowerCase();
|
||||
if (code.length < 4) return false;
|
||||
if (nom.includes('total') || nom.includes('applicatif') || nom.includes('téléphonie')) return false;
|
||||
// Exclure les lignes dont le code ressemble à un montant (ex: "504980")
|
||||
if (/^\d{5,}$/.test(code)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
await conn.execute('DELETE FROM opex_bases_repartition WHERE annee = ?', [annee]);
|
||||
|
||||
const values = etablissements.map(e => [
|
||||
annee,
|
||||
String(e.code).trim(),
|
||||
String(e.nom || '').trim() || null,
|
||||
e.base_repartition || 0,
|
||||
e.base_repartition_hep || 0,
|
||||
]);
|
||||
|
||||
if (values.length > 0) {
|
||||
await conn.query(
|
||||
'INSERT INTO opex_bases_repartition (annee, etablissementCode, etablissementNom, baseRepartition, baseRepartitionHep) VALUES ?',
|
||||
[values]
|
||||
);
|
||||
}
|
||||
|
||||
const totalBase = etablissements.reduce((s, e) => s + (e.base_repartition || 0), 0);
|
||||
const totalHep = etablissements.reduce((s, e) => s + (e.base_repartition_hep || 0), 0);
|
||||
const nbHep = etablissements.filter(e => (e.base_repartition_hep || 0) > 0).length;
|
||||
|
||||
console.log(`Année ${annee}: ${values.length} bases de répartition insérées`);
|
||||
console.log(` Total base standard: ${totalBase.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €`);
|
||||
console.log(` Total base HEP: ${totalHep.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} € (${nbHep} établissements HEP)`);
|
||||
}
|
||||
|
||||
await conn.end();
|
||||
console.log('\n✅ Seed bases de répartition terminé.');
|
||||
139
scripts/seed-opex.mjs
Normal file
139
scripts/seed-opex.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Script de seed : migration des données OPEX 2025 et 2026
|
||||
* depuis les fichiers JSON sources vers la base de données MySQL.
|
||||
*
|
||||
* Usage : node scripts/seed-opex.mjs
|
||||
*/
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = resolve(__dirname, '..');
|
||||
|
||||
// Charger les variables d'environnement
|
||||
dotenv.config({ path: resolve(projectRoot, '.env') });
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
if (!DATABASE_URL) {
|
||||
console.error('❌ DATABASE_URL manquant dans .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Lire les fichiers JSON sources
|
||||
const data2025 = JSON.parse(readFileSync(resolve(projectRoot, 'client/src/data_opex_2025.json'), 'utf-8'));
|
||||
const data2026 = JSON.parse(readFileSync(resolve(projectRoot, 'client/src/data_opex.json'), 'utf-8'));
|
||||
|
||||
async function seedOpex(conn, data) {
|
||||
const annee = data.annee;
|
||||
const postes = data.postes;
|
||||
const etablissements = data.etablissements; // liste ou vide
|
||||
|
||||
console.log(`\n📅 Migration OPEX ${annee} — ${postes.length} postes, ${etablissements?.length ?? 0} établissements`);
|
||||
|
||||
// 1. Vérifier si des données existent déjà pour cette année
|
||||
const [existing] = await conn.execute(
|
||||
'SELECT COUNT(*) as cnt FROM opex_postes WHERE annee = ?',
|
||||
[annee]
|
||||
);
|
||||
const count = existing[0].cnt;
|
||||
|
||||
if (count > 0) {
|
||||
console.log(` ⚠️ ${count} postes déjà présents pour ${annee} — suppression et réimport...`);
|
||||
await conn.execute('DELETE FROM opex_montants_etab WHERE annee = ?', [annee]);
|
||||
await conn.execute('DELETE FROM opex_postes WHERE annee = ?', [annee]);
|
||||
}
|
||||
|
||||
// 2. Insérer les postes
|
||||
const montantKey = `montant_previsionnel_${annee}`;
|
||||
let nbPostesInseres = 0;
|
||||
|
||||
for (const poste of postes) {
|
||||
const montant = poste[montantKey] ?? poste.montant_previsionnel ?? null;
|
||||
await conn.execute(
|
||||
`INSERT INTO opex_postes
|
||||
(annee, colIdx, libelle, libelleCourt, libelleDetail, fournisseur, categorie,
|
||||
type, facturation, modeVentilation, compte, detail, budgetN1, montant, isCustom)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`,
|
||||
[
|
||||
annee,
|
||||
poste.col_idx ?? 0,
|
||||
poste.libelle ?? '',
|
||||
poste.libelle_court ?? null,
|
||||
poste.libelle_detail ?? null,
|
||||
poste.fournisseur ?? null,
|
||||
poste.categorie ?? null,
|
||||
poste.type ?? null,
|
||||
poste.facturation ?? null,
|
||||
poste.mode_ventilation ?? 'Prorata C 6',
|
||||
poste.compte ?? null,
|
||||
poste.detail ?? null,
|
||||
poste.budget_n1 ?? null,
|
||||
montant,
|
||||
]
|
||||
);
|
||||
nbPostesInseres++;
|
||||
}
|
||||
console.log(` ✅ ${nbPostesInseres} postes insérés`);
|
||||
|
||||
// 3. Insérer les montants par établissement (si disponibles) — batch INSERT
|
||||
if (Array.isArray(etablissements) && etablissements.length > 0) {
|
||||
const rows = [];
|
||||
for (const etab of etablissements) {
|
||||
const code = etab.code;
|
||||
const montants = etab.montants ?? {};
|
||||
for (const [libellePoste, montantVal] of Object.entries(montants)) {
|
||||
if (montantVal === null || montantVal === undefined) continue;
|
||||
rows.push([annee, code, libellePoste, montantVal]);
|
||||
}
|
||||
}
|
||||
|
||||
// Insérer par lots de 500 pour éviter les limites MySQL
|
||||
const BATCH_SIZE = 500;
|
||||
let nbMontants = 0;
|
||||
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||
const placeholders = batch.map(() => '(?, ?, ?, ?)').join(', ');
|
||||
const values = batch.flat();
|
||||
await conn.execute(
|
||||
`INSERT INTO opex_montants_etab (annee, etablissementCode, libellePoste, montant)
|
||||
VALUES ${placeholders}`,
|
||||
values
|
||||
);
|
||||
nbMontants += batch.length;
|
||||
}
|
||||
console.log(` ✅ ${nbMontants} montants établissements insérés`);
|
||||
} else {
|
||||
console.log(` ℹ️ Pas de montants par établissement pour ${annee} (seront calculés à la volée)`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Démarrage du seed OPEX...');
|
||||
console.log(`📡 Connexion à la base de données...`);
|
||||
|
||||
const conn = await mysql.createConnection(DATABASE_URL);
|
||||
|
||||
try {
|
||||
await seedOpex(conn, data2025);
|
||||
await seedOpex(conn, data2026);
|
||||
|
||||
// Résumé final
|
||||
const [total] = await conn.execute('SELECT COUNT(*) as cnt FROM opex_postes');
|
||||
const [totalMontants] = await conn.execute('SELECT COUNT(*) as cnt FROM opex_montants_etab');
|
||||
console.log(`\n🎉 Seed terminé !`);
|
||||
console.log(` Total postes en BDD : ${total[0].cnt}`);
|
||||
console.log(` Total montants établissements : ${totalMontants[0].cnt}`);
|
||||
} finally {
|
||||
await conn.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ Erreur lors du seed :', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
63
server/db.ts
63
server/db.ts
@@ -10,6 +10,7 @@ import {
|
||||
InsertUser,
|
||||
inventaireMeta,
|
||||
inventairePostes,
|
||||
opexBasesRepartition,
|
||||
opexMontantsEtab,
|
||||
opexPostes,
|
||||
opexValidated,
|
||||
@@ -326,3 +327,65 @@ export async function insertCapexLignes(rows: InsertCapexLigne[]) {
|
||||
await db.insert(capexLignes).values(rows.slice(i, i + 100));
|
||||
}
|
||||
}
|
||||
|
||||
// ── OPEX Bases de répartition ────────────────────────────────────────────────
|
||||
export async function getOpexBasesRepartition(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
return db
|
||||
.select()
|
||||
.from(opexBasesRepartition)
|
||||
.where(eq(opexBasesRepartition.annee, annee));
|
||||
}
|
||||
|
||||
export async function upsertOpexBaseRepartition(input: {
|
||||
annee: number;
|
||||
etablissementCode: string;
|
||||
etablissementNom?: string | null;
|
||||
baseRepartition: number;
|
||||
baseRepartitionHep?: number;
|
||||
modeManuel?: boolean;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db
|
||||
.insert(opexBasesRepartition)
|
||||
.values({
|
||||
annee: input.annee,
|
||||
etablissementCode: input.etablissementCode,
|
||||
etablissementNom: input.etablissementNom ?? null,
|
||||
baseRepartition: String(input.baseRepartition),
|
||||
baseRepartitionHep: String(input.baseRepartitionHep ?? 0),
|
||||
modeManuel: input.modeManuel ?? false,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
baseRepartition: String(input.baseRepartition),
|
||||
baseRepartitionHep: String(input.baseRepartitionHep ?? 0),
|
||||
etablissementNom: input.etablissementNom ?? null,
|
||||
modeManuel: input.modeManuel ?? false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Import en masse des bases de répartition pour une année (supprime et réinsère) */
|
||||
export async function importOpexBasesRepartition(
|
||||
annee: number,
|
||||
rows: Array<{ etablissementCode: string; etablissementNom?: string | null; baseRepartition: number; baseRepartitionHep?: number }>
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
// Supprimer les données existantes pour l'année
|
||||
await db.delete(opexBasesRepartition).where(eq(opexBasesRepartition.annee, annee));
|
||||
if (rows.length === 0) return;
|
||||
// Insérer en batch
|
||||
await db.insert(opexBasesRepartition).values(
|
||||
rows.map(r => ({
|
||||
annee,
|
||||
etablissementCode: r.etablissementCode,
|
||||
etablissementNom: r.etablissementNom ?? null,
|
||||
baseRepartition: String(r.baseRepartition),
|
||||
baseRepartitionHep: String(r.baseRepartitionHep ?? 0),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,6 +140,28 @@ export const appRouter = router({
|
||||
.input(z.object({ annee: z.number(), etablissementCode: z.string(), libellePoste: z.string(), montant: z.string().nullable() }))
|
||||
.mutation(async ({ input }) => { await db.setOpexMontantEtab(input as Parameters<typeof db.setOpexMontantEtab>[0]); return { success: true }; }),
|
||||
|
||||
// Sauvegarder en masse les montants par établissement (utilisé lors du passage en mode tout manuel)
|
||||
batchSetMontantsEtab: writeProcedure
|
||||
.input(z.object({
|
||||
annee: z.number(),
|
||||
etablissementCode: z.string(),
|
||||
montants: z.array(z.object({
|
||||
libellePoste: z.string(),
|
||||
montant: z.string(),
|
||||
}))
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
for (const m of input.montants) {
|
||||
await db.setOpexMontantEtab({
|
||||
annee: input.annee,
|
||||
etablissementCode: input.etablissementCode,
|
||||
libellePoste: m.libellePoste,
|
||||
montant: m.montant,
|
||||
});
|
||||
}
|
||||
return { success: true, count: input.montants.length };
|
||||
}),
|
||||
|
||||
getValidated: protectedProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
.query(async ({ input }) => db.getOpexValidated(input.annee)),
|
||||
@@ -174,6 +196,39 @@ export const appRouter = router({
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Bases de répartition par établissement (charges classe 6)
|
||||
getBasesRepartition: protectedProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
.query(async ({ input }) => db.getOpexBasesRepartition(input.annee)),
|
||||
|
||||
// Upsert une base de répartition pour un établissement (standard + HEP)
|
||||
setBaseRepartition: writeProcedure
|
||||
.input(z.object({
|
||||
annee: z.number(),
|
||||
etablissementCode: z.string(),
|
||||
etablissementNom: z.string().optional().nullable(),
|
||||
baseRepartition: z.number(),
|
||||
baseRepartitionHep: z.number().optional().default(0),
|
||||
modeManuel: z.boolean().optional().default(false),
|
||||
}))
|
||||
.mutation(async ({ input }) => { await db.upsertOpexBaseRepartition(input); return { success: true }; }),
|
||||
|
||||
// Import en masse des bases de répartition (depuis fichier Excel/CSV)
|
||||
importBasesRepartition: writeProcedure
|
||||
.input(z.object({
|
||||
annee: z.number(),
|
||||
rows: z.array(z.object({
|
||||
etablissementCode: z.string(),
|
||||
etablissementNom: z.string().optional().nullable(),
|
||||
baseRepartition: z.number(),
|
||||
baseRepartitionHep: z.number().optional().default(0),
|
||||
})),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
await db.importOpexBasesRepartition(input.annee, input.rows);
|
||||
return { success: true, count: input.rows.length };
|
||||
}),
|
||||
|
||||
// Supprimer toutes les lignes OPEX d'une année
|
||||
deleteAll: writeProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
|
||||
25
todo.md
25
todo.md
@@ -40,3 +40,28 @@
|
||||
- [ ] Gestion des droits par établissement (userEtablissements)
|
||||
- [ ] Export PDF/Excel des budgets
|
||||
- [ ] Historique des modifications (audit log)
|
||||
|
||||
## Onglet Clés de répartition dans DSI OPEX
|
||||
|
||||
- [x] Analyser le fichier source pour extraire les colonnes base_repartition et base_repartition_hep
|
||||
- [x] Ajouter colonne base_repartition_hep dans la table opex_bases_repartition + migration
|
||||
- [x] Routes tRPC : opex.getBasesRepartition (mise à jour), opex.setBaseRepartition (mise à jour avec hep), opex.importBasesRepartition
|
||||
- [x] Seeder les données base_repartition_hep 2026 depuis le fichier source
|
||||
- [x] Créer l'onglet "Clés de répartition" dans DsiOpex avec tableau éditable (base standard + /HEP)
|
||||
- [x] Import fichier Excel/CSV dans l'onglet Clés de répartition
|
||||
- [x] Afficher l'info "Année de la base = année OPEX - 2" dans l'onglet
|
||||
- [x] Brancher le calcul du poste "DUI pôle HEP" sur base_repartition_hep
|
||||
|
||||
## Corrections de lacunes (post-checkpoint 841ff1bd)
|
||||
|
||||
- [x] Ajouter un vrai loading/error state pour l'onglet « Clés de répartition » basé sur la requête `opex.getBasesRepartition`
|
||||
- [x] Rendre l'import des clés de répartition compatible avec le classeur source réel : détection intelligente de la bonne feuille (mots-clés OPEX/DSI/répartition/base), normalisation des accents, validation des colonnes attendues avec message d'erreur détaillé
|
||||
|
||||
## Bugs et nouvelles fonctionnalités (post-checkpoint cfeb5196)
|
||||
|
||||
- [x] Bug : doublon de ligne dans le tableau Clés de répartition au clic (mutation setBaseRepartition provoque un double rendu)
|
||||
- [x] Fonctionnalité : mode "tout manuel" par établissement dans l'onglet Clés de répartition (toutes les colonnes OPEX de cet établissement saisies manuellement, ignorant le calcul prorata)
|
||||
|
||||
## Corrections (post-checkpoint eb1684f0)
|
||||
|
||||
- [x] Bug : lignes parasites (catégories Infogérance, Sécurité, TOTAL, etc.) dans la vue Établissements OPEX — suppression en BDD + filtre anti-parasites dans l'import
|
||||
|
||||
Reference in New Issue
Block a user