Checkpoint: Application complète : deux tableaux de bord (Veille Stratégique + AAP), import Excel quotidien avec déduplication, sources multiples (local/OneDrive/FTP/SharePoint), affichage liste/vignettes, filtres multi-critères, gestion utilisateurs, logs d'import, page paramètres, authentification locale, tâche cron 06h00, 13 tests Vitest passants.

This commit is contained in:
Manus
2026-03-16 10:45:35 -04:00
parent 5000fc555d
commit 8fb71e8bda
27 changed files with 4525 additions and 184 deletions

View File

@@ -1,17 +1,18 @@
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
import {
boolean,
int,
mysqlEnum,
mysqlTable,
text,
timestamp,
varchar,
json,
} from "drizzle-orm/mysql-core";
// ─── Utilisateurs (Manus OAuth + locaux) ────────────────────────────────────
/**
* Core user table backing auth flow.
* Extend this file with additional tables as your product grows.
* Columns use camelCase to match both database fields and generated types.
*/
export const users = mysqlTable("users", {
/**
* Surrogate primary key. Auto-incremented numeric value managed by the database.
* Use this for relations between tables.
*/
id: int("id").autoincrement().primaryKey(),
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
openId: varchar("openId", { length: 64 }).notNull().unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
@@ -25,4 +26,92 @@ export const users = mysqlTable("users", {
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
// TODO: Add your tables here
// ─── Utilisateurs locaux (auth interne) ─────────────────────────────────────
export const localUsers = mysqlTable("local_users", {
id: int("id").autoincrement().primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
email: varchar("email", { length: 320 }).notNull().unique(),
passwordHash: varchar("passwordHash", { length: 255 }).notNull(),
role: mysqlEnum("role", ["admin", "user", "readonly"]).default("user").notNull(),
isActive: boolean("isActive").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn"),
});
export type LocalUser = typeof localUsers.$inferSelect;
export type InsertLocalUser = typeof localUsers.$inferInsert;
// ─── Paramètres de l'application ────────────────────────────────────────────
export const appSettings = mysqlTable("app_settings", {
id: int("id").autoincrement().primaryKey(),
key: varchar("key", { length: 128 }).notNull().unique(),
value: text("value"),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AppSetting = typeof appSettings.$inferSelect;
export type InsertAppSetting = typeof appSettings.$inferInsert;
// ─── Entrées de veille stratégique ──────────────────────────────────────────
export const veilleItems = mysqlTable("veille_items", {
id: int("id").autoincrement().primaryKey(),
// Clé de déduplication : hash du titre + lien
dedupKey: varchar("dedupKey", { length: 64 }).notNull().unique(),
titre: text("titre").notNull(),
categorie: varchar("categorie", { length: 128 }),
niveau: varchar("niveau", { length: 128 }),
territoire: varchar("territoire", { length: 255 }),
resume: text("resume"),
source: varchar("source", { length: 512 }),
passage: text("passage"),
lien: text("lien"),
// Type de veille (feuille d'origine)
typeVeille: mysqlEnum("typeVeille", ["reglementaire", "concurrentielle", "technologique", "generale"]).notNull(),
// Date extraite de la colonne Source (qui contient parfois une date ISO)
datePublication: timestamp("datePublication"),
importedAt: timestamp("importedAt").defaultNow().notNull(),
});
export type VeilleItem = typeof veilleItems.$inferSelect;
export type InsertVeilleItem = typeof veilleItems.$inferInsert;
// ─── Entrées des appels à projets ────────────────────────────────────────────
export const aapItems = mysqlTable("aap_items", {
id: int("id").autoincrement().primaryKey(),
dedupKey: varchar("dedupKey", { length: 64 }).notNull().unique(),
titre: text("titre").notNull(),
categorie: mysqlEnum("categorie", ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).notNull(),
region: varchar("region", { length: 255 }),
departement: varchar("departement", { length: 255 }),
dateCloture: timestamp("dateCloture"),
datePublication: timestamp("datePublication"),
lien: text("lien"),
importedAt: timestamp("importedAt").defaultNow().notNull(),
});
export type AapItem = typeof aapItems.$inferSelect;
export type InsertAapItem = typeof aapItems.$inferInsert;
// ─── Logs d'import ───────────────────────────────────────────────────────────
export const importLogs = mysqlTable("import_logs", {
id: int("id").autoincrement().primaryKey(),
fileType: mysqlEnum("fileType", ["veille", "aap"]).notNull(),
source: varchar("source", { length: 512 }),
status: mysqlEnum("status", ["success", "partial", "error"]).notNull(),
totalRows: int("totalRows").default(0),
newRows: int("newRows").default(0),
skippedRows: int("skippedRows").default(0),
errorMessage: text("errorMessage"),
details: json("details"),
startedAt: timestamp("startedAt").defaultNow().notNull(),
completedAt: timestamp("completedAt"),
});
export type ImportLog = typeof importLogs.$inferSelect;
export type InsertImportLog = typeof importLogs.$inferInsert;