+ {/* En-tête */}
+
+
+
+
+
+
+
Boîte à idées
+
+ {isAdmin
+ ? "Gérez les questions et suggestions des utilisateurs"
+ : "Posez vos questions et partagez vos suggestions"}
+
+
+
+
+
+
+ {/* Filtres */}
+
+
+ setFiltreRecherche(e.target.value)}
+ className="bg-white border-slate-200"
+ />
+
+
+ {(["tous", "ouvert", "en_cours", "resolu", "ferme"] as const).map((s) => (
+
+ ))}
+
+
+
+ {/* Liste des demandes */}
+ {filteredIdeas.length === 0 ? (
+
+
+
Aucune demande pour le moment
+
Soyez le premier à soumettre une question ou une suggestion.
+
+ ) : (
+
+ {filteredIdeas.map((idea) => {
+ const isExpanded = expandedIds.has(idea.id);
+ const cfg = STATUT_CONFIG[idea.statut];
+ return (
+
+ {/* En-tête de la carte */}
+
toggleExpand(idea.id)}
+ >
+
+
+
+ {cfg.icon}
+ {cfg.label}
+
+
+ {idea.titre}
+
+
+
+
+
+ {idea.userName}
+
+
+
+ {format(new Date(idea.createdAt), "d MMM yyyy à HH:mm", { locale: fr })}
+
+ {idea.reponseAdmin && (
+
+
+ Réponse disponible
+
+ )}
+
+
+
+ {isAdmin && (
+
+ )}
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Contenu développé */}
+ {isExpanded && (
+
+ {/* Message */}
+
+
+ Message
+
+
{idea.message}
+
+
+ {/* Réponse admin */}
+ {idea.reponseAdmin && (
+
+
+
+ Réponse de l'équipe
+ {idea.reponduPar && (
+
+ — {idea.reponduPar}
+
+ )}
+ {idea.reponduAt && (
+
+ ({format(new Date(idea.reponduAt), "d MMM yyyy", { locale: fr })})
+
+ )}
+
+
+ {idea.reponseAdmin}
+
+
+ )}
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ {/* Dialog : Nouvelle demande */}
+
+
+ {/* Dialog : Réponse admin */}
+
+
+ );
+}
diff --git a/drizzle/0002_sticky_the_watchers.sql b/drizzle/0002_sticky_the_watchers.sql
new file mode 100644
index 0000000..cf85c14
--- /dev/null
+++ b/drizzle/0002_sticky_the_watchers.sql
@@ -0,0 +1,14 @@
+CREATE TABLE `ideas` (
+ `id` int AUTO_INCREMENT NOT NULL,
+ `userId` int NOT NULL,
+ `userName` varchar(255) NOT NULL,
+ `titre` varchar(512) NOT NULL,
+ `message` text NOT NULL,
+ `statut` enum('ouvert','en_cours','resolu','ferme') NOT NULL DEFAULT 'ouvert',
+ `reponseAdmin` text,
+ `reponduPar` varchar(255),
+ `reponduAt` timestamp,
+ `createdAt` timestamp NOT NULL DEFAULT (now()),
+ `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
+ CONSTRAINT `ideas_id` PRIMARY KEY(`id`)
+);
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..ab060d4
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,663 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "46aedace-2016-466b-a1f0-6dc05f8f383e",
+ "prevId": "8f5ba9eb-1f6d-4c0f-8eeb-262ea8031bee",
+ "tables": {
+ "aap_items": {
+ "name": "aap_items",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "dedupKey": {
+ "name": "dedupKey",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "titre": {
+ "name": "titre",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "categorie": {
+ "name": "categorie",
+ "type": "enum('Handicap','PA','Enfance','Précarité','Sanitaire','Autre')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "region": {
+ "name": "region",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "departement": {
+ "name": "departement",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dateCloture": {
+ "name": "dateCloture",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "datePublication": {
+ "name": "datePublication",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lien": {
+ "name": "lien",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "importedAt": {
+ "name": "importedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "aap_items_id": {
+ "name": "aap_items_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "aap_items_dedupKey_unique": {
+ "name": "aap_items_dedupKey_unique",
+ "columns": [
+ "dedupKey"
+ ]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "app_settings": {
+ "name": "app_settings",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "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": {
+ "app_settings_id": {
+ "name": "app_settings_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "app_settings_key_unique": {
+ "name": "app_settings_key_unique",
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "ideas": {
+ "name": "ideas",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userName": {
+ "name": "userName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "titre": {
+ "name": "titre",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "statut": {
+ "name": "statut",
+ "type": "enum('ouvert','en_cours','resolu','ferme')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'ouvert'"
+ },
+ "reponseAdmin": {
+ "name": "reponseAdmin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reponduPar": {
+ "name": "reponduPar",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reponduAt": {
+ "name": "reponduAt",
+ "type": "timestamp",
+ "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": {
+ "ideas_id": {
+ "name": "ideas_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "import_logs": {
+ "name": "import_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "fileType": {
+ "name": "fileType",
+ "type": "enum('veille','aap')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "enum('success','partial','error')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "totalRows": {
+ "name": "totalRows",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "newRows": {
+ "name": "newRows",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "skippedRows": {
+ "name": "skippedRows",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "errorMessage": {
+ "name": "errorMessage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "details": {
+ "name": "details",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "startedAt": {
+ "name": "startedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "completedAt": {
+ "name": "completedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "import_logs_id": {
+ "name": "import_logs_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "local_users": {
+ "name": "local_users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "passwordHash": {
+ "name": "passwordHash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "enum('admin','user','readonly')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "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": {
+ "local_users_id": {
+ "name": "local_users_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "local_users_email_unique": {
+ "name": "local_users_email_unique",
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "openId": {
+ "name": "openId",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "loginMethod": {
+ "name": "loginMethod",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "enum('user','admin')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "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": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "users_id": {
+ "name": "users_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "users_openId_unique": {
+ "name": "users_openId_unique",
+ "columns": [
+ "openId"
+ ]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "veille_items": {
+ "name": "veille_items",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "dedupKey": {
+ "name": "dedupKey",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "titre": {
+ "name": "titre",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "categorie": {
+ "name": "categorie",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "niveau": {
+ "name": "niveau",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "territoire": {
+ "name": "territoire",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "resume": {
+ "name": "resume",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "passage": {
+ "name": "passage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lien": {
+ "name": "lien",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "typeVeille": {
+ "name": "typeVeille",
+ "type": "enum('reglementaire','concurrentielle','technologique','generale')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "datePublication": {
+ "name": "datePublication",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "importedAt": {
+ "name": "importedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "veille_items_id": {
+ "name": "veille_items_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "veille_items_dedupKey_unique": {
+ "name": "veille_items_dedupKey_unique",
+ "columns": [
+ "dedupKey"
+ ]
+ }
+ },
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index cfcbba5..55e15ce 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1773671031809,
"tag": "0001_old_shocker",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "5",
+ "when": 1776437589031,
+ "tag": "0002_sticky_the_watchers",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index 5ec111e..871d075 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -115,3 +115,22 @@ export const importLogs = mysqlTable("import_logs", {
export type ImportLog = typeof importLogs.$inferSelect;
export type InsertImportLog = typeof importLogs.$inferInsert;
+
+// ─── Boîte à idées ───────────────────────────────────────────────────────────
+
+export const ideas = mysqlTable("ideas", {
+ id: int("id").autoincrement().primaryKey(),
+ userId: int("userId").notNull(),
+ userName: varchar("userName", { length: 255 }).notNull(),
+ titre: varchar("titre", { length: 512 }).notNull(),
+ message: text("message").notNull(),
+ statut: mysqlEnum("statut", ["ouvert", "en_cours", "resolu", "ferme"]).default("ouvert").notNull(),
+ reponseAdmin: text("reponseAdmin"),
+ reponduPar: varchar("reponduPar", { length: 255 }),
+ reponduAt: timestamp("reponduAt"),
+ createdAt: timestamp("createdAt").defaultNow().notNull(),
+ updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
+});
+
+export type Idea = typeof ideas.$inferSelect;
+export type InsertIdea = typeof ideas.$inferInsert;
diff --git a/server/db.ts b/server/db.ts
index bce1572..00b5f83 100644
--- a/server/db.ts
+++ b/server/db.ts
@@ -9,6 +9,8 @@ import {
appSettings,
importLogs,
InsertLocalUser,
+ ideas,
+ InsertIdea,
} from "../drizzle/schema";
import { ENV } from "./_core/env";
@@ -315,3 +317,45 @@ export async function getImportStats() {
totalNewRows,
};
}
+
+// ─── Boîte à idées ────────────────────────────────────────────────────────────
+
+export async function createIdea(data: InsertIdea) {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ await db.insert(ideas).values(data);
+}
+
+export async function getAllIdeas() {
+ const db = await getDb();
+ if (!db) return [];
+ return db.select().from(ideas).orderBy(desc(ideas.createdAt));
+}
+
+export async function getIdeasByUser(userId: number) {
+ const db = await getDb();
+ if (!db) return [];
+ return db.select().from(ideas).where(eq(ideas.userId, userId)).orderBy(desc(ideas.createdAt));
+}
+
+export async function repondreIdea(
+ id: number,
+ reponseAdmin: string,
+ reponduPar: string,
+ statut: "ouvert" | "en_cours" | "resolu" | "ferme"
+) {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ await db.update(ideas).set({
+ reponseAdmin,
+ reponduPar,
+ reponduAt: new Date(),
+ statut,
+ }).where(eq(ideas.id, id));
+}
+
+export async function updateIdeaStatut(id: number, statut: "ouvert" | "en_cours" | "resolu" | "ferme") {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ await db.update(ideas).set({ statut }).where(eq(ideas.id, id));
+}
diff --git a/server/routers.ts b/server/routers.ts
index 23e4351..19b371e 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -17,6 +17,11 @@ import {
createLocalUser,
updateLocalUser,
deleteLocalUser,
+ createIdea,
+ getAllIdeas,
+ getIdeasByUser,
+ repondreIdea,
+ updateIdeaStatut,
} from "./db";
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
@@ -238,6 +243,67 @@ export const appRouter = router({
return { success: true };
}),
}),
+
+ // ─── Boîte à idées ───────────────────────────────────────────────────────────
+ ideas: router({
+ // Créer une nouvelle idée / question
+ create: protectedProcedure
+ .input(
+ z.object({
+ titre: z.string().min(3).max(512),
+ message: z.string().min(10),
+ })
+ )
+ .mutation(async ({ input, ctx }) => {
+ await createIdea({
+ userId: ctx.user.id,
+ userName: ctx.user.name ?? ctx.user.email ?? "Utilisateur",
+ titre: input.titre,
+ message: input.message,
+ });
+ return { success: true };
+ }),
+
+ // Lister toutes les idées (admin) ou les siennes (user)
+ list: protectedProcedure.query(async ({ ctx }) => {
+ if (ctx.user.role === "admin") {
+ return getAllIdeas();
+ }
+ return getIdeasByUser(ctx.user.id);
+ }),
+
+ // Répondre à une idée (admin uniquement)
+ repondre: adminProcedure
+ .input(
+ z.object({
+ id: z.number().int().positive(),
+ reponseAdmin: z.string().min(1),
+ statut: z.enum(["ouvert", "en_cours", "resolu", "ferme"]),
+ })
+ )
+ .mutation(async ({ input, ctx }) => {
+ await repondreIdea(
+ input.id,
+ input.reponseAdmin,
+ ctx.user.name ?? ctx.user.email ?? "Admin",
+ input.statut
+ );
+ return { success: true };
+ }),
+
+ // Changer le statut (admin uniquement)
+ updateStatut: adminProcedure
+ .input(
+ z.object({
+ id: z.number().int().positive(),
+ statut: z.enum(["ouvert", "en_cours", "resolu", "ferme"]),
+ })
+ )
+ .mutation(async ({ input }) => {
+ await updateIdeaStatut(input.id, input.statut);
+ return { success: true };
+ }),
+ }),
});
export type AppRouter = typeof appRouter;
diff --git a/todo.md b/todo.md
index e60476b..006b68a 100644
--- a/todo.md
+++ b/todo.md
@@ -46,3 +46,10 @@
- [x] Page Login : supprimer l'encart affichant les identifiants du compte par défaut
- [x] Login : accepter un identifiant (e-mail ou nom d'utilisateur) au lieu d'un e-mail obligatoire
- [x] Page Login : logo Itinova en haut, "powered by" + logo Santinova en bas
+
+## Boîte à idées
+- [x] BDD : table ideas (id, userId, titre, message, statut, réponseAdmin, createdAt, updatedAt)
+- [x] API tRPC : créer une idée, lister toutes les idées (admin), mes idées (user), répondre (admin)
+- [x] Page BoiteAIdees : liste avec date, titre, demandeur, texte, statut, réponse admin
+- [x] Bouton "Nouvelle demande" en haut à droite ouvrant une boîte de dialogue
+- [x] Menu "Boîte à idées" dans la sidebar accessible à tous les utilisateurs