From c50f4448cb020468d870a5d34bb9752fd5bdad55 Mon Sep 17 00:00:00 2001 From: Marius Arnaud Date: Tue, 28 Jul 2026 11:28:26 +0200 Subject: [PATCH 1/4] feat: add plannedDays to missions and mission consumption reporting - Add plannedDays field to Mission schema (Drizzle + TypeScript) - Update POST/PATCH /api/missions endpoints to handle plannedDays - Add GET /api/reporting/mission-consumption endpoint for overconsumption tracking - Create MissionForm.tsx component with plannedDays input - Generate Drizzle migration for planned_days column --- bun.lock | 10 +- .../db/migrations/pg/0002_empty_maelstrom.sql | 1 + .../db/migrations/pg/meta/0002_snapshot.json | 777 ++++++++++++++++++ .../src/db/migrations/pg/meta/_journal.json | 7 + packages/backend/src/db/schema/pg.schema.ts | 1 + packages/backend/src/lib/schemas.ts | 2 + packages/backend/src/routes/missions.ts | 6 +- packages/backend/src/routes/reporting.ts | 48 +- .../src/components/missions/MissionForm.tsx | 88 ++ packages/shared/src/types/mission.ts | 3 + 10 files changed, 935 insertions(+), 8 deletions(-) create mode 100644 packages/backend/src/db/migrations/pg/0002_empty_maelstrom.sql create mode 100644 packages/backend/src/db/migrations/pg/meta/0002_snapshot.json create mode 100644 packages/frontend/src/components/missions/MissionForm.tsx diff --git a/bun.lock b/bun.lock index c2ace76..c01aacd 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/backend": { "name": "@presto/backend", - "version": "1.23.2", + "version": "1.24.0", "dependencies": { "@hono/zod-validator": "^0.7.6", "@presto/shared": "workspace:*", @@ -50,7 +50,7 @@ }, "packages/frontend": { "name": "@presto/frontend", - "version": "1.23.2", + "version": "1.24.0", "dependencies": { "@presto/shared": "workspace:*", "@tanstack/react-query": "^5.95.2", @@ -75,12 +75,12 @@ }, "packages/shared": { "name": "@presto/shared", - "version": "1.23.2", + "version": "1.24.0", "dependencies": { - "date-holidays": "latest", + "date-holidays": "^3.26.11", }, "devDependencies": { - "typescript": "latest", + "typescript": "^6.0.2", }, }, }, diff --git a/packages/backend/src/db/migrations/pg/0002_empty_maelstrom.sql b/packages/backend/src/db/migrations/pg/0002_empty_maelstrom.sql new file mode 100644 index 0000000..fa15edd --- /dev/null +++ b/packages/backend/src/db/migrations/pg/0002_empty_maelstrom.sql @@ -0,0 +1 @@ +ALTER TABLE "Mission" ADD COLUMN "planned_days" integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/packages/backend/src/db/migrations/pg/meta/0002_snapshot.json b/packages/backend/src/db/migrations/pg/meta/0002_snapshot.json new file mode 100644 index 0000000..f906a88 --- /dev/null +++ b/packages/backend/src/db/migrations/pg/meta/0002_snapshot.json @@ -0,0 +1,777 @@ +{ + "id": "9ecd17dd-e1f0-440e-9506-e62f860cd5e1", + "prevId": "988929c4-e870-4924-82ff-143371edf6cc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ActivityReport": { + "name": "ActivityReport", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "month": { + "name": "month", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ReportStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "totalDays": { + "name": "totalDays", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_note": { + "name": "private_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dailyRate": { + "name": "dailyRate", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "holidayCountry": { + "name": "holidayCountry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "missionId": { + "name": "missionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ActivityReport_userId_year_month_idx": { + "name": "ActivityReport_userId_year_month_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ActivityReport_missionId_Mission_id_fk": { + "name": "ActivityReport_missionId_Mission_id_fk", + "tableFrom": "ActivityReport", + "tableTo": "Mission", + "columnsFrom": [ + "missionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ActivityReport_userId_User_id_fk": { + "name": "ActivityReport_userId_User_id_fk", + "tableFrom": "ActivityReport", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ActivityReport_missionId_month_year_key": { + "name": "ActivityReport_missionId_month_year_key", + "nullsNotDistinct": false, + "columns": [ + "missionId", + "month", + "year" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Client": { + "name": "Client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "businessId": { + "name": "businessId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "holidayCountry": { + "name": "holidayCountry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Client_userId_idx": { + "name": "Client_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Client_userId_User_id_fk": { + "name": "Client_userId_User_id_fk", + "tableFrom": "Client", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Company": { + "name": "Company", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "businessId": { + "name": "businessId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Company_userId_idx": { + "name": "Company_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Company_userId_User_id_fk": { + "name": "Company_userId_User_id_fk", + "tableFrom": "Company", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Mission": { + "name": "Mission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "companyId": { + "name": "companyId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dailyRate": { + "name": "dailyRate", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "startDate": { + "name": "startDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endDate": { + "name": "endDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "planned_days": { + "name": "planned_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Mission_userId_idx": { + "name": "Mission_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Mission_clientId_idx": { + "name": "Mission_clientId_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Mission_companyId_idx": { + "name": "Mission_companyId_idx", + "columns": [ + { + "expression": "companyId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Mission_clientId_Client_id_fk": { + "name": "Mission_clientId_Client_id_fk", + "tableFrom": "Mission", + "tableTo": "Client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "Mission_companyId_Company_id_fk": { + "name": "Mission_companyId_Company_id_fk", + "tableFrom": "Mission", + "tableTo": "Company", + "columnsFrom": [ + "companyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "Mission_userId_User_id_fk": { + "name": "Mission_userId_User_id_fk", + "tableFrom": "Mission", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ReportEntry": { + "name": "ReportEntry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isWeekend": { + "name": "isWeekend", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isHoliday": { + "name": "isHoliday", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reportId": { + "name": "reportId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ReportEntry_reportId_ActivityReport_id_fk": { + "name": "ReportEntry_reportId_ActivityReport_id_fk", + "tableFrom": "ReportEntry", + "tableTo": "ActivityReport", + "columnsFrom": [ + "reportId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ReportEntry_reportId_date_key": { + "name": "ReportEntry_reportId_date_key", + "nullsNotDistinct": false, + "columns": [ + "reportId", + "date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.UserSettings": { + "name": "UserSettings", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "baseCurrency": { + "name": "baseCurrency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'EUR'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "UserSettings_userId_User_id_fk": { + "name": "UserSettings_userId_User_id_fk", + "tableFrom": "UserSettings", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_email_unique": { + "name": "User_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ReportStatus": { + "name": "ReportStatus", + "schema": "public", + "values": [ + "DRAFT", + "COMPLETED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/src/db/migrations/pg/meta/_journal.json b/packages/backend/src/db/migrations/pg/meta/_journal.json index bad10ab..56411d4 100644 --- a/packages/backend/src/db/migrations/pg/meta/_journal.json +++ b/packages/backend/src/db/migrations/pg/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1774360291803, "tag": "0001_parched_thena", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1785230734669, + "tag": "0002_empty_maelstrom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/src/db/schema/pg.schema.ts b/packages/backend/src/db/schema/pg.schema.ts index e41aa63..ac3f5a2 100644 --- a/packages/backend/src/db/schema/pg.schema.ts +++ b/packages/backend/src/db/schema/pg.schema.ts @@ -113,6 +113,7 @@ export const missions = pgTable( startDate: timestamp("startDate", { precision: 3, mode: "date" }), endDate: timestamp("endDate", { precision: 3, mode: "date" }), isActive: boolean("isActive").notNull().default(true), + plannedDays: integer("planned_days").notNull().default(0), // Nombre de jours prévus pour la mission createdAt: createdAt(), updatedAt: updatedAt(), }, diff --git a/packages/backend/src/lib/schemas.ts b/packages/backend/src/lib/schemas.ts index e3f2078..dad1cc4 100644 --- a/packages/backend/src/lib/schemas.ts +++ b/packages/backend/src/lib/schemas.ts @@ -91,6 +91,7 @@ export const createMissionSchema = z dailyRate: z.number().min(0).optional(), startDate: dateString.optional(), endDate: dateString.optional(), + plannedDays: z.number().min(0).optional(), // Optionnel, 0 par défaut }) .refine((d) => !d.startDate || !d.endDate || d.endDate >= d.startDate, { message: "End date must be on or after start date", @@ -106,6 +107,7 @@ export const updateMissionSchema = z startDate: dateString.nullable().optional(), endDate: dateString.nullable().optional(), isActive: z.boolean().optional(), + plannedDays: z.number().min(0).optional(), // Optionnel }) .refine((d) => !d.startDate || !d.endDate || d.endDate >= d.startDate, { message: "End date must be on or after start date", diff --git a/packages/backend/src/routes/missions.ts b/packages/backend/src/routes/missions.ts index a5cfbdd..4d9cb08 100644 --- a/packages/backend/src/routes/missions.ts +++ b/packages/backend/src/routes/missions.ts @@ -29,7 +29,7 @@ missionsRouter.get("/", async (c) => { missionsRouter.post("/", zValidator("json", createMissionSchema), async (c) => { const userId = c.get("userId"); - const { name, clientId, companyId, dailyRate, startDate, endDate } = c.req.valid("json"); + const { name, clientId, companyId, dailyRate, startDate, endDate, plannedDays } = c.req.valid("json"); await findOwned("client", clientId, userId); await findOwned("company", companyId, userId); @@ -42,6 +42,7 @@ missionsRouter.post("/", zValidator("json", createMissionSchema), async (c) => { dailyRate: dailyRate ?? null, startDate: startDate ? new Date(startDate) : null, endDate: endDate ? new Date(endDate) : null, + plannedDays: plannedDays ?? 0, // Valeur par défaut : 0 }); c.header("Location", `/api/missions/${mission.id}`); @@ -61,11 +62,12 @@ missionsRouter.patch("/:id", zValidator("json", updateMissionSchema), async (c) await findOwned("company", data.companyId, userId); } - const { startDate, endDate, ...rest } = data; + const { startDate, endDate, plannedDays, ...rest } = data; await updateReturning(missions, id, { ...rest, ...(startDate !== undefined && { startDate: startDate ? new Date(startDate) : null }), ...(endDate !== undefined && { endDate: endDate ? new Date(endDate) : null }), + ...(plannedDays !== undefined && { plannedDays }), // Mise à jour si fourni updatedAt: new Date(), }); diff --git a/packages/backend/src/routes/reporting.ts b/packages/backend/src/routes/reporting.ts index 9afb3fe..5a2d629 100644 --- a/packages/backend/src/routes/reporting.ts +++ b/packages/backend/src/routes/reporting.ts @@ -1,7 +1,8 @@ -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; import { db, userSettings } from "../db/index.js"; +import { activityReports, missions } from "../db/schema/pg.schema.js"; import { logger } from "../lib/logger.js"; import type { AppEnv } from "../lib/types.js"; import { parseIntParam } from "../lib/utils.js"; @@ -34,4 +35,49 @@ reporting.get("/", async (c) => { } }); +// Nouvel endpoint pour la consommation des missions +reporting.get("/mission-consumption", async (c) => { + const userId = c.get("userId"); + const year = parseIntParam(c.req.query("year"), "year", 2000, 2100) ?? new Date().getUTCFullYear(); + + // Récupérer les missions avec plannedDays + const missionsWithPlannedDays = await db + .select({ + missionId: missions.id, + missionName: missions.name, + plannedDays: missions.plannedDays, + clientId: missions.clientId, + }) + .from(missions) + .where(eq(missions.userId, userId)); + + // Calculer les jours travaillés par mission (via activity_reports) + const daysWorkedByMission = await db + .select({ + missionId: activityReports.missionId, + daysWorked: sql`SUM(${activityReports.totalDays})`.as("days_worked"), + }) + .from(activityReports) + .where(sql`EXTRACT(YEAR FROM ${activityReports.createdAt}) = ${year} AND ${activityReports.userId} = ${userId}`) + .groupBy(activityReports.missionId); + + // Fusionner les données + const consumptionData = missionsWithPlannedDays.map((mission) => { + const daysWorkedEntry = daysWorkedByMission.find((dw) => dw.missionId === mission.missionId); + const daysWorked = daysWorkedEntry?.daysWorked || 0; + const isOverconsumed = daysWorked > mission.plannedDays; + + return { + missionId: mission.missionId, + missionName: mission.missionName, + clientId: mission.clientId, + plannedDays: mission.plannedDays, + daysWorked, + isOverconsumed, + }; + }); + + return c.json(consumptionData); +}); + export default reporting; diff --git a/packages/frontend/src/components/missions/MissionForm.tsx b/packages/frontend/src/components/missions/MissionForm.tsx new file mode 100644 index 0000000..7707edd --- /dev/null +++ b/packages/frontend/src/components/missions/MissionForm.tsx @@ -0,0 +1,88 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import type { CreateMissionRequest, Mission } from "@presto/shared"; +import { createMissionSchema } from "@presto/shared"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/components/ui/use-toast"; + +interface MissionFormProps { + defaultValues?: Partial; + onSubmit: (data: CreateMissionRequest) => Promise; + isLoading?: boolean; +} + +export function MissionForm({ defaultValues, onSubmit, isLoading }: MissionFormProps) { + const { t } = useTranslation(); + const { toast } = useToast(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const form = useForm({ + resolver: zodResolver(createMissionSchema), + defaultValues: { + name: "", + clientId: "", + companyId: "", + dailyRate: undefined, + startDate: undefined, + endDate: undefined, + plannedDays: 0, // Valeur par défaut + ...defaultValues, + }, + }); + + useEffect(() => { + if (defaultValues) { + form.reset(defaultValues); + } + }, [defaultValues, form]); + + async function handleSubmit(data: CreateMissionRequest) { + try { + await onSubmit(data); + toast({ + title: t("mission.form.success.title"), + description: t("mission.form.success.description"), + }); + queryClient.invalidateQueries({ queryKey: ["missions"] }); + navigate("/missions"); + } catch (_error) { + toast({ + title: t("mission.form.error.title"), + description: t("mission.form.error.description"), + variant: "destructive", + }); + } + } + + return ( +
+
+
+ + + {form.formState.errors.name && ( +

{form.formState.errors.name.message}

+ )} +
+
+ + + {form.formState.errors.plannedDays && ( +

{form.formState.errors.plannedDays.message}

+ )} +
+
+ {/* Autres champs (clientId, companyId, dailyRate, etc.) */} + +
+ ); +} diff --git a/packages/shared/src/types/mission.ts b/packages/shared/src/types/mission.ts index f9627f8..5ab19c5 100644 --- a/packages/shared/src/types/mission.ts +++ b/packages/shared/src/types/mission.ts @@ -11,6 +11,7 @@ export interface Mission { startDate: string | null; endDate: string | null; isActive: boolean; + plannedDays: number; // Nombre de jours prévus pour la mission createdAt: string; updatedAt: string; client?: { @@ -32,6 +33,7 @@ export interface CreateMissionRequest { dailyRate?: number; startDate?: string; endDate?: string; + plannedDays?: number; // Optionnel, 0 par défaut } export interface UpdateMissionRequest { @@ -42,4 +44,5 @@ export interface UpdateMissionRequest { startDate?: string | null; endDate?: string | null; isActive?: boolean; + plannedDays?: number; // Optionnel } From a5c875d1b508194ed39c4044bcad6f473dd39bc6 Mon Sep 17 00:00:00 2001 From: Marius Arnaud Date: Tue, 28 Jul 2026 11:50:30 +0200 Subject: [PATCH 2/4] fix: resolve linting issues and typecheck blockers - Fix noArrayIndexKey warnings in MissionConsumptionChart, Skeleton, and Dashboard - Remove temporary