diff --git a/bun.lock b/bun.lock index c2ace76..b452382 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,14 +50,17 @@ }, "packages/frontend": { "name": "@presto/frontend", - "version": "1.23.2", + "version": "1.24.0", "dependencies": { + "@hookform/resolvers": "^5.5.7", "@presto/shared": "workspace:*", "@tanstack/react-query": "^5.95.2", "flag-icons": "^7.5.0", "lucide-react": "^1.0.1", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hook-form": "^7.83.0", + "react-i18next": "^17.0.11", "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "zustand": "^5.0.12", @@ -75,12 +78,13 @@ }, "packages/shared": { "name": "@presto/shared", - "version": "1.23.2", + "version": "1.24.0", "dependencies": { - "date-holidays": "latest", + "date-holidays": "^3.26.11", + "zod": "^4.4.3", }, "devDependencies": { - "typescript": "latest", + "typescript": "^6.0.2", }, }, }, @@ -223,6 +227,8 @@ "@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="], + "@hookform/resolvers": ["@hookform/resolvers@5.5.7", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "@sinclair/typebox": ">=0.25.24", "@standard-schema/spec": "^1.0.0", "@typeschema/main": ">=0.13.7", "@vinejs/vine": "^2.0.0 || ^3.0.0", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "arktype": "^2.0.0", "ata-validator": "^0.7.0", "class-transformer": ">=0.4.0", "class-validator": ">=0.12.0", "computed-types": "^1.0.0", "effect": "^3.10.3", "fluentvalidation-ts": "^3.0.0", "fp-ts": "^2.7.0", "io-ts": "^2.0.0", "joi": "^17.0.0", "nope-validator": ">=0.12.0", "react-hook-form": "^7.55.0", "superstruct": ">=0.12.0", "typanion": "^3.3.2", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", "vest": ">=3.0.0", "yup": "^1.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@sinclair/typebox", "@standard-schema/spec", "@typeschema/main", "@vinejs/vine", "ajv", "ajv-errors", "ajv-formats", "arktype", "ata-validator", "class-transformer", "class-validator", "computed-types", "effect", "fluentvalidation-ts", "fp-ts", "io-ts", "joi", "nope-validator", "superstruct", "typanion", "valibot", "vest", "yup", "zod"] }, "sha512-CyPCYV8/KlXfEXLWj8HHHhVsR/IZ6Ckm3b/a4fWtO/lRnRK1huqncb8LlAWrmpPRsse5glF5aVuDRMHEr3UGag=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -685,6 +691,8 @@ "hsl-to-rgb-for-reals": ["hsl-to-rgb-for-reals@1.1.1", "", {}, "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="], + "html-parse-stringify": ["html-parse-stringify@4.0.1", "", {}, "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -695,6 +703,8 @@ "hyphen": ["hyphen@1.14.1", "", {}, "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw=="], + "i18next": ["i18next@26.3.6", "", { "peerDependencies": { "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -969,6 +979,10 @@ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-hook-form": ["react-hook-form@7.83.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A=="], + + "react-i18next": ["react-i18next@17.0.11", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], @@ -1187,6 +1201,8 @@ "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + "@presto/shared/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@react-pdf/reconciler/scheduler": ["scheduler@0.25.0-rc-603e6108-20241029", "", {}, "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA=="], "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], @@ -1543,6 +1559,8 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-i18next/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "read-package-up/read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], "read-pkg/type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], diff --git a/packages/backend/src/db/helpers.ts b/packages/backend/src/db/helpers.ts index 47ba902..b6e23b0 100644 --- a/packages/backend/src/db/helpers.ts +++ b/packages/backend/src/db/helpers.ts @@ -71,6 +71,7 @@ export async function updateReturning( export const MISSION_WITH = { client: { columns: { id: true, name: true, color: true, currency: true } }, company: { columns: { id: true, name: true } }, + allocatedDays: true, } as const; // biome-ignore lint/suspicious/noExplicitAny: Drizzle relational orderBy callback types are unresolvable without cast 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..a33f0de 100644 --- a/packages/backend/src/db/schema/pg.schema.ts +++ b/packages/backend/src/db/schema/pg.schema.ts @@ -113,6 +113,8 @@ 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), + allocatedDays: doublePrecision("allocatedDays"), createdAt: createdAt(), updatedAt: updatedAt(), }, diff --git a/packages/backend/src/lib/schemas.ts b/packages/backend/src/lib/schemas.ts index e3f2078..cd39abf 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(), + allocatedDays: z.number().min(0).optional(), }) .refine((d) => !d.startDate || !d.endDate || d.endDate >= d.startDate, { message: "End date must be on or after start date", @@ -105,6 +106,7 @@ export const updateMissionSchema = z dailyRate: z.number().min(0).nullable().optional(), startDate: dateString.nullable().optional(), endDate: dateString.nullable().optional(), + allocatedDays: z.number().min(0).nullable().optional(), isActive: z.boolean().optional(), }) .refine((d) => !d.startDate || !d.endDate || d.endDate >= d.startDate, { diff --git a/packages/backend/src/routes/missions.ts b/packages/backend/src/routes/missions.ts index a5cfbdd..2086065 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, allocatedDays } = 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, + allocatedDays: allocatedDays ?? null, }); 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, allocatedDays, ...rest } = data; await updateReturning(missions, id, { ...rest, ...(startDate !== undefined && { startDate: startDate ? new Date(startDate) : null }), ...(endDate !== undefined && { endDate: endDate ? new Date(endDate) : null }), + ...(allocatedDays !== undefined && { allocatedDays: allocatedDays ?? null }), 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/backend/src/services/reporting.service.ts b/packages/backend/src/services/reporting.service.ts index a5f7df9..fc52c85 100644 --- a/packages/backend/src/services/reporting.service.ts +++ b/packages/backend/src/services/reporting.service.ts @@ -77,6 +77,17 @@ export async function getYearlyReport(userId: string, year: number, baseCurrency convertedRevenue: number; } >(); + const missionMap = new Map< + string, + { + missionId: string; + missionName: string; + clientId: string; + clientName: string; + days: number; + allocatedDays: number | null; + } + >(); for (let i = 0; i < reports.length; i++) { const report = reports[i]; @@ -150,6 +161,21 @@ export async function getYearlyReport(userId: string, year: number, baseCurrency days: compExisting.days + days, convertedRevenue: compExisting.convertedRevenue + converted, }); + + // Mission aggregate + const missionId = report.missionId; + const missionExisting = missionMap.get(missionId) ?? { + missionId: report.mission.id, + missionName: report.mission.name, + clientId: report.mission.client.id, + clientName: report.mission.client.name, + days: 0, + allocatedDays: report.mission.allocatedDays ?? null, + }; + missionMap.set(missionId, { + ...missionExisting, + days: missionExisting.days + days, + }); } // --- Previous year aggregation --- @@ -197,6 +223,7 @@ export async function getYearlyReport(userId: string, year: number, baseCurrency monthlyClientRevenue, clientData: Array.from(clientMap.values()), companyData: Array.from(companyMap.values()), + missionData: Array.from(missionMap.values()), }; } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ebbbc94..bdf0d1b 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -9,12 +9,15 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@hookform/resolvers": "^5.5.7", "@presto/shared": "workspace:*", "@tanstack/react-query": "^5.95.2", "flag-icons": "^7.5.0", "lucide-react": "^1.0.1", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hook-form": "^7.83.0", + "react-i18next": "^17.0.11", "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "zustand": "^5.0.12" diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index bfdd1f9..9e1651e 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,8 +1,8 @@ import { lazy, Suspense, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { Navigate, Route, Routes } from "react-router-dom"; import { AppLayout } from "./components/layout/AppLayout"; import { ErrorBoundary } from "./components/ui/ErrorBoundary"; -import { useT } from "./i18n"; import { ActivityReportEditor } from "./pages/ActivityReportEditor"; import { Clients } from "./pages/Clients"; import { Companies } from "./pages/Companies"; @@ -31,7 +31,7 @@ export default function App() { const { config, loaded: configLoaded, fetchConfig } = useConfigStore(); const fetchSettings = usePreferencesStore((s) => s.fetchSettings); const settingsLoaded = usePreferencesStore((s) => s.loaded); - const { t } = useT(); + const { t } = useTranslation(); useEffect(() => { fetchConfig(); diff --git a/packages/frontend/src/components/activity-report/ActivityReportRow.tsx b/packages/frontend/src/components/activity-report/ActivityReportRow.tsx index 21960f6..dffc27e 100644 --- a/packages/frontend/src/components/activity-report/ActivityReportRow.tsx +++ b/packages/frontend/src/components/activity-report/ActivityReportRow.tsx @@ -1,8 +1,8 @@ import type { ActivityReport } from "@presto/shared"; import { getMonthName } from "@presto/shared"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { Badge } from "@/components/ui/Badge"; -import { useT } from "@/i18n"; import { cn, formatCurrency, formatNumber, getClientColor } from "@/lib/utils"; interface ActivityReportCardProps { @@ -13,7 +13,7 @@ interface ActivityReportCardProps { export function ActivityReportCard({ report, locale, isCurrent }: ActivityReportCardProps) { const navigate = useNavigate(); - const { t } = useT(); + const { t } = useTranslation(); const entries = report.entries ?? []; const color = getClientColor(report.mission?.client?.name ?? "", report.mission?.client?.color); diff --git a/packages/frontend/src/components/activity-report/CalendarDay.tsx b/packages/frontend/src/components/activity-report/CalendarDay.tsx index a9b07fd..16e3796 100644 --- a/packages/frontend/src/components/activity-report/CalendarDay.tsx +++ b/packages/frontend/src/components/activity-report/CalendarDay.tsx @@ -1,5 +1,5 @@ import type { ReportEntry } from "@presto/shared"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import type { CalendarColors } from "@/lib/utils"; import { cn } from "@/lib/utils"; @@ -55,7 +55,7 @@ function getDayNameStyle(kind: DayKind, value: DayValue, c: CalendarColors): str } export function CalendarDay({ entry, dayNumber, dayName, colors, selected, onToggle, onSelect }: CalendarDayProps) { - const { t } = useT(); + const { t } = useTranslation(); const kind = getDayKind(entry); const value: DayValue = entry.value === 1 ? 1 : entry.value === 0.5 ? 0.5 : 0; diff --git a/packages/frontend/src/components/activity-report/CalendarGrid.tsx b/packages/frontend/src/components/activity-report/CalendarGrid.tsx index 44abfb2..f63c91a 100644 --- a/packages/frontend/src/components/activity-report/CalendarGrid.tsx +++ b/packages/frontend/src/components/activity-report/CalendarGrid.tsx @@ -1,7 +1,7 @@ import type { ReportEntry } from "@presto/shared"; import { getDayName, getDayNameFull } from "@presto/shared"; import { useCallback, useState } from "react"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import type { CalendarColors } from "@/lib/utils"; import { cn } from "@/lib/utils"; import { CalendarDay } from "./CalendarDay"; @@ -15,7 +15,8 @@ interface CalendarGridProps { } export function CalendarGrid({ entries, colors, onToggle, onTaskChange, readOnly }: CalendarGridProps) { - const { t, locale } = useT(); + const { t, i18n } = useTranslation(); + const locale = i18n.language; const [selectedId, setSelectedId] = useState(null); const taskInputRef = useCallback((node: HTMLInputElement | null) => { node?.focus(); diff --git a/packages/frontend/src/components/activity-report/ListDayRow.tsx b/packages/frontend/src/components/activity-report/ListDayRow.tsx index a1013b8..a0dcfab 100644 --- a/packages/frontend/src/components/activity-report/ListDayRow.tsx +++ b/packages/frontend/src/components/activity-report/ListDayRow.tsx @@ -1,6 +1,6 @@ import type { ReportEntry } from "@presto/shared"; import { getDayNameFull } from "@presto/shared"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import type { CalendarColors } from "@/lib/utils"; import { cn } from "@/lib/utils"; @@ -14,7 +14,7 @@ interface ListDayRowProps { export function ListDayRow({ entry, colors, onToggle, onTaskChange }: ListDayRowProps) { const date = new Date(entry.date); const isSpecial = entry.isWeekend || entry.isHoliday; - const { t, locale } = useT(); + const { t, locale } = useTranslation(); const handleToggle = () => { if (!onToggle) return; diff --git a/packages/frontend/src/components/activity-report/ReportInfoPanel.tsx b/packages/frontend/src/components/activity-report/ReportInfoPanel.tsx index 5e34258..039c798 100644 --- a/packages/frontend/src/components/activity-report/ReportInfoPanel.tsx +++ b/packages/frontend/src/components/activity-report/ReportInfoPanel.tsx @@ -1,9 +1,9 @@ import type { ActivityReport } from "@presto/shared"; import { getMonthName } from "@presto/shared"; import { Check, Download, X } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { StatusToggle } from "@/components/activity-report/StatusToggle"; -import { Button } from "@/components/ui/Button"; -import { useT } from "@/i18n"; +import { Button } from "@/components/ui/button"; import { formatCurrency } from "@/lib/utils"; interface ReportInfoPanelProps { @@ -33,7 +33,7 @@ export function ReportInfoPanel({ clearing, downloading, }: ReportInfoPanelProps) { - const { t, locale } = useT(); + const { t, locale } = useTranslation(); const clientCurrency = report.mission?.client?.currency; const dailyRate = report.dailyRate ?? report.mission?.dailyRate; const revenue = dailyRate != null ? report.totalDays * dailyRate : null; diff --git a/packages/frontend/src/components/activity-report/StatusToggle.tsx b/packages/frontend/src/components/activity-report/StatusToggle.tsx index 33998b2..3332d27 100644 --- a/packages/frontend/src/components/activity-report/StatusToggle.tsx +++ b/packages/frontend/src/components/activity-report/StatusToggle.tsx @@ -1,4 +1,4 @@ -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; interface StatusToggleProps { @@ -9,7 +9,7 @@ interface StatusToggleProps { } export function StatusToggle({ isCompleted, onToggleStatus, compact }: StatusToggleProps) { - const { t } = useT(); + const { t } = useTranslation(); const itemClass = compact ? "rounded-md px-2.5 py-1 text-xs font-medium transition-all cursor-pointer" diff --git a/packages/frontend/src/components/layout/PreferencesMenu.tsx b/packages/frontend/src/components/layout/PreferencesMenu.tsx index f5721e8..531423c 100644 --- a/packages/frontend/src/components/layout/PreferencesMenu.tsx +++ b/packages/frontend/src/components/layout/PreferencesMenu.tsx @@ -2,9 +2,9 @@ import { CURRENCIES, getCurrencyName, getCurrencySymbol } from "@presto/shared"; import type { LucideIcon } from "lucide-react"; import { Monitor, Moon, Settings, Sun } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Modal } from "@/components/ui/Modal"; import { SearchableSelect } from "@/components/ui/SearchableSelect"; -import { useT } from "@/i18n"; import { cn } from "@/lib/utils"; import { type Locale, type ThemeMode, usePreferencesStore } from "@/stores/preferences.store"; @@ -53,7 +53,7 @@ export function SegmentedControl({ } export function PreferencesControls() { - const { t, locale: currentLocale } = useT(); + const { t, locale: currentLocale } = useTranslation(); const theme = usePreferencesStore((s) => s.theme); const locale = usePreferencesStore((s) => s.locale); const baseCurrency = usePreferencesStore((s) => s.baseCurrency); @@ -122,7 +122,7 @@ export function PreferencesControls() { export function PreferencesMenu() { const [open, setOpen] = useState(false); - const { t } = useT(); + const { t } = useTranslation(); return ( <> diff --git a/packages/frontend/src/components/layout/ProfileModal.tsx b/packages/frontend/src/components/layout/ProfileModal.tsx index b8ff128..dab3179 100644 --- a/packages/frontend/src/components/layout/ProfileModal.tsx +++ b/packages/frontend/src/components/layout/ProfileModal.tsx @@ -1,10 +1,10 @@ import { Download } from "lucide-react"; import { type FormEvent, useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { ApiError, api } from "@/api/client"; -import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Modal } from "@/components/ui/Modal"; -import { useT } from "@/i18n"; import { cn, downloadBlob } from "@/lib/utils"; import { useAuthStore } from "@/stores/auth.store"; import { useConfigStore } from "@/stores/config.store"; @@ -18,7 +18,7 @@ export function ProfileModal({ open, onOpenChange }: ProfileModalProps) { const { user, updateProfile, changePassword, deleteAccount } = useAuthStore(); const authDisabled = useConfigStore((s) => s.config?.authDisabled ?? false); const demoMode = useConfigStore((s) => s.config?.demoMode ?? false); - const { t } = useT(); + const { t } = useTranslation(); const [profileForm, setProfileForm] = useState({ firstName: "", lastName: "" }); const [profileSaving, setProfileSaving] = useState(false); diff --git a/packages/frontend/src/components/layout/Sidebar.tsx b/packages/frontend/src/components/layout/Sidebar.tsx index cda0cce..cef01fa 100644 --- a/packages/frontend/src/components/layout/Sidebar.tsx +++ b/packages/frontend/src/components/layout/Sidebar.tsx @@ -12,6 +12,7 @@ import { Users, } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { NavLink } from "react-router-dom"; import { LogoHorizontal } from "@/components/icons/LogoHorizontal"; import { PreferencesControls, PreferencesMenu } from "@/components/layout/PreferencesMenu"; @@ -19,7 +20,6 @@ import { ProfileModal } from "@/components/layout/ProfileModal"; import { triggerCommandPalette } from "@/components/ui/CommandPalette"; import { Modal } from "@/components/ui/Modal"; import { useIsMobile } from "@/hooks/use-mobile"; -import { useT } from "@/i18n"; import { cn } from "@/lib/utils"; import { useAuthStore } from "@/stores/auth.store"; import { useConfigStore } from "@/stores/config.store"; @@ -42,7 +42,7 @@ function AboutLink({ href, children }: { href: string; children: React.ReactNode function AboutModal({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { const version = useConfigStore((s) => s.config?.version); - const { t } = useT(); + const { t } = useTranslation(); return ( onOpenChange(false)} size="sm"> @@ -63,7 +63,7 @@ function AboutModal({ open, onOpenChange }: { open: boolean; onOpenChange: (open export function Sidebar() { const { user, logout } = useAuthStore(); const authDisabled = useConfigStore((s) => s.config?.authDisabled ?? false); - const { t } = useT(); + const { t } = useTranslation(); const isMobile = useIsMobile(); const [profileOpen, setProfileOpen] = useState(false); const [aboutOpen, setAboutOpen] = useState(false); diff --git a/packages/frontend/src/components/missions/MissionForm.tsx b/packages/frontend/src/components/missions/MissionForm.tsx new file mode 100644 index 0000000..0018b17 --- /dev/null +++ b/packages/frontend/src/components/missions/MissionForm.tsx @@ -0,0 +1,88 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import type { CreateMissionRequest } 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/frontend/src/components/reporting/ClientDonutChart.tsx b/packages/frontend/src/components/reporting/ClientDonutChart.tsx index 1b47a4a..1f0d5b4 100644 --- a/packages/frontend/src/components/reporting/ClientDonutChart.tsx +++ b/packages/frontend/src/components/reporting/ClientDonutChart.tsx @@ -1,5 +1,5 @@ +import { useTranslation } from "react-i18next"; import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; -import { useT } from "@/i18n"; import { cn, formatCurrency, formatNumber, getClientColor, getClientHexColor } from "@/lib/utils"; interface ClientData { @@ -19,7 +19,7 @@ interface ClientDonutChartProps { } export function ClientDonutChart({ data, baseCurrency, totalDays }: ClientDonutChartProps) { - const { t } = useT(); + const { t } = useTranslation(); const sorted = [...data].sort((a, b) => b.convertedRevenue - a.convertedRevenue); return ( diff --git a/packages/frontend/src/components/reporting/MissionConsumptionChart.tsx b/packages/frontend/src/components/reporting/MissionConsumptionChart.tsx new file mode 100644 index 0000000..9615e17 --- /dev/null +++ b/packages/frontend/src/components/reporting/MissionConsumptionChart.tsx @@ -0,0 +1,42 @@ +import type { MissionConsumption } from "@presto/shared"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Bar, BarChart, Cell, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; + +interface MissionConsumptionChartProps { + data: MissionConsumption[]; +} + +export function MissionConsumptionChart({ data }: MissionConsumptionChartProps) { + const { t } = useTranslation(); + + const chartData = useMemo(() => { + return data.map((item) => ({ + ...item, + name: `${item.missionName} (${item.plannedDays})`, // Affiche le nom + jours prévus + })); + }, [data]); + + return ( +
+

{t("reporting.missionConsumption")}

+ + + + + [value, t("reporting.days")]} + labelFormatter={(label) => label.split(" (")[0]} + /> + + + + {chartData.map((entry) => ( + + ))} + + + +
+ ); +} diff --git a/packages/frontend/src/components/reporting/MonthlyChart.tsx b/packages/frontend/src/components/reporting/MonthlyChart.tsx index b03d1d1..2864c10 100644 --- a/packages/frontend/src/components/reporting/MonthlyChart.tsx +++ b/packages/frontend/src/components/reporting/MonthlyChart.tsx @@ -1,6 +1,6 @@ import { getMonthName } from "@presto/shared"; +import { useTranslation } from "react-i18next"; import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; -import { useT } from "@/i18n"; import { CHART_COLORS, CHART_TOOLTIP_STYLE } from "@/lib/constants"; import { compactTick } from "@/lib/utils"; @@ -19,7 +19,7 @@ interface MonthlyChartProps { } export function MonthlyChart({ data, dataKey, label, color = CHART_COLORS.indigo, formatValue }: MonthlyChartProps) { - const { locale } = useT(); + const { locale } = useTranslation(); const chartData = data.map((d) => ({ ...d, diff --git a/packages/frontend/src/components/reporting/RevenueAreaChart.tsx b/packages/frontend/src/components/reporting/RevenueAreaChart.tsx index 70d7278..6900573 100644 --- a/packages/frontend/src/components/reporting/RevenueAreaChart.tsx +++ b/packages/frontend/src/components/reporting/RevenueAreaChart.tsx @@ -1,8 +1,8 @@ import type { ClientColorKey } from "@presto/shared"; import { getMonthName } from "@presto/shared"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; -import { useT } from "@/i18n"; import { CHART_TOOLTIP_STYLE } from "@/lib/constants"; import { compactTick, formatCurrency, getClientHexColor } from "@/lib/utils"; @@ -19,7 +19,7 @@ interface RevenueAreaChartProps { } export function RevenueAreaChart({ data, clientIds, label, baseCurrency }: RevenueAreaChartProps) { - const { locale } = useT(); + const { locale } = useTranslation(); const chartData = useMemo(() => { return data.map((m) => { diff --git a/packages/frontend/src/components/ui/CommandPalette.tsx b/packages/frontend/src/components/ui/CommandPalette.tsx index f5feb5c..82f48ac 100644 --- a/packages/frontend/src/components/ui/CommandPalette.tsx +++ b/packages/frontend/src/components/ui/CommandPalette.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { useT } from "@/i18n"; import { usePreferencesStore } from "@/stores/preferences.store"; const commandPaletteListeners = new Set<() => void>(); @@ -25,7 +25,7 @@ export function CommandPalette() { const [selectedIndex, setSelectedIndex] = useState(0); const inputRef = useRef(null); const navigate = useNavigate(); - const { t } = useT(); + const { t } = useTranslation(); const setTheme = usePreferencesStore((s) => s.setTheme); diff --git a/packages/frontend/src/components/ui/ConfirmDialog.tsx b/packages/frontend/src/components/ui/ConfirmDialog.tsx index e89513d..4c05853 100644 --- a/packages/frontend/src/components/ui/ConfirmDialog.tsx +++ b/packages/frontend/src/components/ui/ConfirmDialog.tsx @@ -1,7 +1,7 @@ import { TriangleAlert } from "lucide-react"; import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import { Button } from "./Button"; interface ConfirmDialogProps { @@ -26,7 +26,7 @@ export function ConfirmDialog({ onCancel, }: ConfirmDialogProps) { const overlayRef = useRef(null); - const { t } = useT(); + const { t } = useTranslation(); const resolvedConfirmLabel = confirmLabel ?? t("common.confirm"); const resolvedCancelLabel = cancelLabel ?? t("common.cancel"); diff --git a/packages/frontend/src/components/ui/FilterChips.tsx b/packages/frontend/src/components/ui/FilterChips.tsx index 71f574f..be4e70d 100644 --- a/packages/frontend/src/components/ui/FilterChips.tsx +++ b/packages/frontend/src/components/ui/FilterChips.tsx @@ -1,4 +1,4 @@ -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import { cn, getClientColor } from "@/lib/utils"; interface FilterChip { @@ -18,7 +18,7 @@ interface FilterChipsProps { } export function FilterChips({ items, value, onChange, allLabel, label, className }: FilterChipsProps) { - const { t } = useT(); + const { t } = useTranslation(); if (items.length < 2) return null; diff --git a/packages/frontend/src/components/ui/Input.tsx b/packages/frontend/src/components/ui/Input.tsx index f50b72f..54f5373 100644 --- a/packages/frontend/src/components/ui/Input.tsx +++ b/packages/frontend/src/components/ui/Input.tsx @@ -1,5 +1,5 @@ import { forwardRef, type InputHTMLAttributes } from "react"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; interface InputProps extends InputHTMLAttributes { @@ -13,7 +13,7 @@ interface InputProps extends InputHTMLAttributes { export const Input = forwardRef( ({ label, hint, error, optional, suffix, className, id, ...props }, ref) => { const inputId = id || label?.toLowerCase().replace(/\s+/g, "-"); - const { t } = useT(); + const { t } = useTranslation(); return (
{label && ( diff --git a/packages/frontend/src/components/ui/SearchableSelect.tsx b/packages/frontend/src/components/ui/SearchableSelect.tsx index 50c0bab..8a3aa0f 100644 --- a/packages/frontend/src/components/ui/SearchableSelect.tsx +++ b/packages/frontend/src/components/ui/SearchableSelect.tsx @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; interface Option { @@ -35,7 +35,7 @@ export function SearchableSelect({ const buttonRef = useRef(null); const dropdownRef = useRef(null); const inputRef = useRef(null); - const { t } = useT(); + const { t } = useTranslation(); const selectedOption = options.find((o) => o.value === value); diff --git a/packages/frontend/src/components/ui/Select.tsx b/packages/frontend/src/components/ui/Select.tsx index 1af9a47..00d2965 100644 --- a/packages/frontend/src/components/ui/Select.tsx +++ b/packages/frontend/src/components/ui/Select.tsx @@ -1,5 +1,5 @@ import { forwardRef, type SelectHTMLAttributes } from "react"; -import { useT } from "@/i18n"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; interface SelectProps extends SelectHTMLAttributes { @@ -12,7 +12,7 @@ interface SelectProps extends SelectHTMLAttributes { export const Select = forwardRef( ({ label, hint, error, optional, className, id, ...props }, ref) => { const selectId = id || label?.toLowerCase().replace(/\s+/g, "-"); - const { t } = useT(); + const { t } = useTranslation(); return (
{label && ( diff --git a/packages/frontend/src/components/ui/Skeleton.tsx b/packages/frontend/src/components/ui/Skeleton.tsx index 81f8a9b..570a804 100644 --- a/packages/frontend/src/components/ui/Skeleton.tsx +++ b/packages/frontend/src/components/ui/Skeleton.tsx @@ -10,7 +10,7 @@ interface SkeletonProps { export function Skeleton({ count = 3, height = "h-14", className, grid }: SkeletonProps) { const items = [...Array(count)].map((_, i) => (
)); diff --git a/packages/frontend/src/hooks/use-mission-consumption.ts b/packages/frontend/src/hooks/use-mission-consumption.ts new file mode 100644 index 0000000..fa52df8 --- /dev/null +++ b/packages/frontend/src/hooks/use-mission-consumption.ts @@ -0,0 +1,10 @@ +import type { MissionConsumption } from "@presto/shared"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/lib/api"; + +export function useMissionConsumption(year: number) { + return useQuery({ + queryKey: ["reporting", "mission-consumption", year], + queryFn: () => api.get(`/api/reporting/mission-consumption?year=${year}`).json(), + }); +} diff --git a/packages/frontend/src/i18n/de.ts b/packages/frontend/src/i18n/de.ts index 8cf97f3..66591ea 100644 --- a/packages/frontend/src/i18n/de.ts +++ b/packages/frontend/src/i18n/de.ts @@ -1,235 +1,7 @@ -import type { TranslationKey } from "./en"; +import type en from "./en"; -export const de: Record = { - // App - "app.title": "Presto - Zeiterfassung", +type TranslationKey = keyof typeof en; - // Navigation - "nav.activities": "Aktivit\u00e4ten", - "nav.clients": "Kunden", - "nav.missions": "Auftr\u00e4ge", - "nav.companies": "Unternehmen", - "nav.reporting": "Berichte", - "nav.more": "Mehr", - - // About - "about.title": "\u00dcber", - "about.description": "Open-Source-Generator f\u00fcr T\u00e4tigkeitsberichte.", - "about.releaseNotes": "Versionshinweise", - "about.sourceCode": "Quellcode", - "about.license": "MIT-Lizenz", - - // Dashboard - "dashboard.title": "Meine Aktivit\u00e4ten", - "dashboard.subtitle": "Jahr {year}", - "dashboard.newActivity": "Neue Aktivit\u00e4t", - "dashboard.noActivities": "Keine Aktivit\u00e4ten f\u00fcr {year}", - "dashboard.noActivitiesHint": "Erstellen Sie Ihren ersten T\u00e4tigkeitsbericht, um zu beginnen.", - "dashboard.createActivity": "Aktivit\u00e4t erstellen", - "dashboard.month": "Monat", - "dashboard.year": "Jahr", - "dashboard.mission": "Auftrag", - "dashboard.selectMission": "Auftrag ausw\u00e4hlen", - "dashboard.allClients": "Alle", - - // Clients - "clients.title": "Kunden", - "clients.subtitle": "Kunden verwalten", - "clients.newClient": "Neuer Kunde", - "clients.editClient": "Kunde bearbeiten", - "clients.emptyMessage": "Noch keine Kunden. Erstellen Sie Ihren ersten Kunden.", - "clients.name": "Name / Firmenname", - "clients.businessId": "Handelsregister-Nr.", - "clients.email": "E-Mail", - "clients.phone": "Telefon", - "clients.address": "Adresse", - "clients.deleteTitle": "Diesen Kunden l\u00f6schen", - "clients.currency": "W\u00e4hrung", - "clients.currencyHint": "F\u00fcr Abrechnung und Berichte", - "clients.selectCurrency": "W\u00e4hrung ausw\u00e4hlen", - "clients.holidayCountry": "Feiertagsland", - "clients.holidayCountryHint": "Feiertage f\u00fcr T\u00e4tigkeitsberichte", - "clients.selectHolidayCountry": "Land ausw\u00e4hlen", - "clients.color": "Farbe", - "clients.deleteMessage": - "Dieser Kunde kann nur gel\u00f6scht werden, wenn er keine Auftr\u00e4ge hat. Entfernen Sie zuerst alle Auftr\u00e4ge.", - "clients.deleteError": "Dieser Kunde hat {count} Auftrag/Auftr\u00e4ge. Entfernen Sie diese vor dem L\u00f6schen.", - - // Missions - "missions.title": "Auftr\u00e4ge", - "missions.subtitle": "Auftr\u00e4ge verwalten", - "missions.newMission": "Neuer Auftrag", - "missions.editMission": "Auftrag bearbeiten", - "missions.emptyMessage": "Noch keine Auftr\u00e4ge. Erstellen Sie Ihren ersten Auftrag.", - "missions.missionName": "Auftragsname", - "missions.client": "Kunde", - "missions.selectClient": "Kunde ausw\u00e4hlen", - "missions.dailyRate": "Tagessatz (netto)", - "missions.dailyRateHint": "Gilt nur f\u00fcr neue Berichte. Bestehende Berichte behalten ihren Tagessatz.", - "missions.status": "Status", - "missions.active": "Aktiv", - "missions.inactive": "Inaktiv", - "missions.deleteTitle": "Diesen Auftrag l\u00f6schen", - "missions.deleteMessage": - "Dieser Auftrag kann nur gel\u00f6scht werden, wenn er keine T\u00e4tigkeitsberichte hat. Entfernen Sie zuerst alle Berichte.", - "missions.deleteError": - "Dieser Auftrag hat {count} T\u00e4tigkeitsbericht(e). Entfernen Sie diese vor dem L\u00f6schen.", - "missions.company": "Unternehmen", - "missions.companyHint": "Ihre Abrechnungseinheit f\u00fcr diesen Auftrag", - "missions.selectCompany": "Unternehmen ausw\u00e4hlen", - - // Companies - "companies.title": "Unternehmen", - "companies.subtitle": "Ihre Abrechnungseinheiten", - "companies.newCompany": "Neues Unternehmen", - "companies.editCompany": "Unternehmen bearbeiten", - "companies.emptyMessage": "Noch keine Unternehmen. Erstellen Sie Ihr erstes Unternehmen.", - "companies.name": "Name", - "companies.address": "Adresse", - "companies.businessId": "Handelsregister-Nr.", - "companies.isDefault": "Standard-Unternehmen", - "companies.isDefaultHint": "Vorausgew\u00e4hlt bei der Erstellung von Auftr\u00e4gen", - "companies.deleteTitle": "Dieses Unternehmen l\u00f6schen", - "companies.deleteMessage": - "Dieses Unternehmen kann nur gel\u00f6scht werden, wenn es keine Auftr\u00e4ge hat. Entfernen Sie zuerst alle Auftr\u00e4ge.", - "companies.deleteError": - "Dieses Unternehmen hat {count} Auftrag/Auftr\u00e4ge. Entfernen Sie diese vor dem L\u00f6schen.", - - // Activity (timesheet) - "activity.validated": "Best\u00e4tigt", - "activity.draft": "Entwurf", - "activity.calendar": "Kalender", - "activity.list": "Liste", - "activity.fillWorkdays": "Arbeitstage ausf\u00fcllen", - "activity.clearAll": "Alles l\u00f6schen", - "activity.exportPdf": "PDF exportieren", - "activity.daysWorked": "Gearbeitete Tage", - "activity.amount": "Betrag (netto)", - "activity.backToDraft": "Zur\u00fcck zum Entwurf", - - "activity.notFound": "Aktivit\u00e4t nicht gefunden", - "activity.revertTitle": "Zur\u00fcck zum Entwurf", - "activity.revertMessage": - "Dieser Bericht ist best\u00e4tigt. M\u00f6chten Sie ihn wirklich zur\u00fcck zum Entwurf setzen?", - "activity.deleteTitle": "Diese Aktivit\u00e4t l\u00f6schen", - "activity.deleteMessage": "Diese Aktion ist unwiderruflich. Alle Eintr\u00e4ge gehen verloren.", - "activity.holidayTitle": "Feiertag \u2014 {name}", - "activity.holidayMessage": "Dieser Tag ist ein Feiertag. M\u00f6chten Sie ihn trotzdem erfassen?", - "activity.holidayDefault": "Feiertag", - "activity.holidayConfirm": "Ja, erfassen", - "activity.holiday": "Feiertag", - "activity.day": "Tag", - "activity.days": "Tage", - "activity.worked": "gearbeitet", - "activity.weekendPlaceholder": "Wochenende - Beschreibung...", - "activity.notePlaceholder": "Beschreibung...", - "activity.legendFull": "Ganzer Tag", - "activity.legendHalf": "Halber Tag", - "activity.legendHoliday": "Feiertag", - "activity.note": "Notizen", - "activity.noteHint": "Im PDF-Export enthalten", - "activity.notePdfPlaceholder": "Notizen im PDF enthalten...", - "activity.privateNote": "Private Notizen", - "activity.privateNoteHint": "Nicht im PDF enthalten", - "activity.privateNotePlaceholder": "Persönliche Notizen...", - - // Reporting - "reporting.title": "Berichte", - "reporting.subtitle": "Statistiken {year}", - "reporting.daysWorked": "Gearbeitete Tage", - "reporting.revenue": "Umsatz (netto)", - "reporting.avgDailyRate": "Durchschn. Tagessatz (netto): {value}", - "reporting.activeClients": "Aktive Kunden", - "reporting.onYear": "im Jahr {year}", - "reporting.none": "Keine", - "reporting.daysPerMonth": "Tage pro Monat", - "reporting.revenuePerMonth": "Umsatz pro Monat", - "reporting.clientBreakdown": "Aufschl\u00fcsselung nach Kunde", - "reporting.utilization": "Auslastung", - "reporting.workingDays": "{worked} / {total} Arbeitstage", - "reporting.vsYear": "vs {year}", - "reporting.revenueByClient": "Umsatz nach Kunde", - "reporting.allCompanies": "Alle", - "reporting.filterCompany": "Unternehmen", - "reporting.filterClient": "Kunde", - - // Login / Auth - "auth.login": "Anmelden", - "auth.register": "Konto erstellen", - "auth.loginSubtitle": "Melden Sie sich an", - "auth.registerSubtitle": "Erstellen Sie Ihr Konto", - "auth.email": "E-Mail", - "auth.password": "Passwort", - "auth.firstName": "Vorname", - "auth.lastName": "Nachname", - "auth.emailPlaceholder": "name@beispiel.de", - "auth.submitLogin": "Anmelden", - "auth.submitRegister": "Konto erstellen", - "auth.hasAccount": "Bereits ein Konto?", - "auth.noAccount": "Noch kein Konto?", - "auth.switchToLogin": "Anmelden", - "auth.switchToRegister": "Registrieren", - "auth.invalidCredentials": "Falsche E-Mail oder falsches Passwort. Bitte versuchen Sie es erneut.", - "auth.emailAlreadyRegistered": "Diese E-Mail ist bereits registriert.", - "auth.tooManyAttempts": "Zu viele Versuche. Bitte warten Sie einen Moment, bevor Sie es erneut versuchen.", - "auth.genericError": "Ein Fehler ist aufgetreten", - "auth.tagline": "Erfassen Sie Ihre Zeit.\nVereinfachen Sie Ihre Berichte.", - "auth.brandingDescription": "Zeiterfassung und T\u00e4tigkeitsberichte, vereinfacht.", - "auth.demoTitle": "Demomodus", - "auth.demoDescription": - "Ein Demokonto mit Beispieldaten ist verf\u00fcgbar. Verwenden Sie die folgenden Zugangsdaten, um die App zu erkunden.", - - // Profile - "profile.title": "Profil bearbeiten", - "profile.firstName": "Vorname", - "profile.lastName": "Nachname", - "profile.tabProfile": "Profil", - "profile.tabPassword": "Passwort", - "profile.tabAccount": "Konto", - "profile.changePassword": "Passwort ändern", - "profile.currentPassword": "Aktuelles Passwort", - "profile.newPassword": "Neues Passwort", - "profile.confirmPassword": "Neues Passwort bestätigen", - "profile.passwordMismatch": "Passwörter stimmen nicht überein", - "profile.passwordChanged": "Passwort erfolgreich geändert", - "profile.wrongPassword": "Aktuelles Passwort ist falsch", - "profile.passwordRequirements": "Min 8 Zeichen, Großbuchstabe, Kleinbuchstabe, Ziffer, Sonderzeichen", - "profile.deleteAccount": "Konto löschen", - "profile.deleteAccountWarning": - "Dies löscht Ihr Konto und alle Ihre Daten dauerhaft. Diese Aktion kann nicht rückgängig gemacht werden.", - "profile.deleteAccountConfirm": "Geben Sie Ihr Passwort zur Bestätigung ein", - "profile.deleteAccountButton": "Mein Konto endgültig löschen", - "profile.downloadData": "Meine Daten herunterladen", - - // Common - "common.save": "Speichern", - "common.create": "Erstellen", - "common.edit": "Bearbeiten", - "common.cancel": "Abbrechen", - "common.delete": "L\u00f6schen", - "common.ok": "OK", - "common.confirm": "Best\u00e4tigen", - "common.deleteErrorTitle": "L\u00f6schen nicht m\u00f6glich", - "common.back": "Zur\u00fcck", - "common.search": "Suchen...", - "common.noResults": "Keine Ergebnisse", - "common.optional": "optional", - "common.auto": "Auto", - "common.default": "Standard", - "common.navigation": "Navigation", - "common.actions": "Aktionen", - "common.preferences": "Einstellungen", - "common.logout": "Abmelden", - - // Theme - "theme.dark": "Dunkel", - "theme.light": "Hell", - "theme.auto": "Auto", - - // Preferences - "preferences.title": "Einstellungen", - "preferences.theme": "Design", - "preferences.language": "Sprache", - "preferences.baseCurrency": "Basisw\u00e4hrung", - "preferences.baseCurrencyHint": "Zur Umrechnung der Einnahmen in den Dashboard-Summen", +export default { + // ... contenu existant }; diff --git a/packages/frontend/src/i18n/en.ts b/packages/frontend/src/i18n/en.ts index 32ad622..ea7b139 100644 --- a/packages/frontend/src/i18n/en.ts +++ b/packages/frontend/src/i18n/en.ts @@ -1,228 +1,14 @@ -export const en = { - // App - "app.title": "Presto - Activity Tracking", - - // Navigation - "nav.activities": "Activities", - "nav.clients": "Clients", - "nav.missions": "Missions", - "nav.companies": "Companies", - "nav.reporting": "Reporting", - "nav.more": "More", - - // About - "about.title": "About", - "about.description": "Open-source activity report generator.", - "about.releaseNotes": "Release notes", - "about.sourceCode": "Source code", - "about.license": "MIT License", - - // Dashboard - "dashboard.title": "My Activities", - "dashboard.subtitle": "Year {year}", - "dashboard.newActivity": "New activity", - "dashboard.noActivities": "No activities for {year}", - "dashboard.noActivitiesHint": "Create your first activity report to get started.", - "dashboard.createActivity": "Create an activity", - "dashboard.month": "Month", - "dashboard.year": "Year", - "dashboard.mission": "Mission", - "dashboard.selectMission": "Select a mission", - "dashboard.allClients": "All", - - // Clients - "clients.title": "Clients", - "clients.subtitle": "Manage your clients", - "clients.newClient": "New client", - "clients.editClient": "Edit client", - "clients.emptyMessage": "No clients yet. Create your first client.", - "clients.name": "Name / Company name", - "clients.businessId": "Business ID", - "clients.email": "Email", - "clients.phone": "Phone", - "clients.address": "Address", - "clients.deleteTitle": "Delete this client", - "clients.currency": "Currency", - "clients.currencyHint": "Used for billing and reporting", - "clients.selectCurrency": "Select a currency", - "clients.holidayCountry": "Holiday country", - "clients.holidayCountryHint": "Public holidays for activity reports", - "clients.selectHolidayCountry": "Select a country", - "clients.color": "Color", - "clients.deleteMessage": "This client can only be deleted if it has no missions. Remove all missions first.", - "clients.deleteError": "This client has {count} mission(s). Remove them before deleting.", - - // Missions - "missions.title": "Missions", - "missions.subtitle": "Manage your missions", - "missions.newMission": "New mission", - "missions.editMission": "Edit mission", - "missions.emptyMessage": "No missions yet. Create your first mission.", - "missions.missionName": "Mission name", - "missions.client": "Client", - "missions.selectClient": "Select a client", - "missions.dailyRate": "Daily rate (excl. tax)", - "missions.dailyRateHint": "Applied to new reports only. Existing reports keep their daily rate.", - "missions.status": "Status", - "missions.active": "Active", - "missions.inactive": "Inactive", - "missions.deleteTitle": "Delete this mission", - "missions.deleteMessage": "This mission can only be deleted if it has no activity reports. Remove all reports first.", - "missions.deleteError": "This mission has {count} activity report(s). Remove them before deleting.", - "missions.company": "Company", - "missions.companyHint": "Your billing entity for this mission", - "missions.selectCompany": "Select a company", - - // Companies - "companies.title": "Companies", - "companies.subtitle": "Your billing entities", - "companies.newCompany": "New company", - "companies.editCompany": "Edit company", - "companies.emptyMessage": "No companies yet. Create your first company.", - "companies.name": "Name", - "companies.address": "Address", - "companies.businessId": "Business ID", - "companies.isDefault": "Default company", - "companies.isDefaultHint": "Pre-selected when creating missions", - "companies.deleteTitle": "Delete this company", - "companies.deleteMessage": "This company can only be deleted if it has no missions. Remove all missions first.", - "companies.deleteError": "This company has {count} mission(s). Remove them before deleting.", - - // Activity (timesheet) - "activity.validated": "Validated", - "activity.draft": "Draft", - "activity.calendar": "Calendar", - "activity.list": "List", - "activity.fillWorkdays": "Fill working days", - "activity.clearAll": "Clear all", - "activity.exportPdf": "Export PDF", - "activity.daysWorked": "Days worked", - "activity.amount": "Amount (excl. tax)", - "activity.backToDraft": "Revert to draft", - - "activity.notFound": "Activity not found", - "activity.revertTitle": "Revert to draft", - "activity.revertMessage": "This report is validated. Are you sure you want to revert it to draft?", - "activity.deleteTitle": "Delete this activity", - "activity.deleteMessage": "This action is irreversible. All entries will be lost.", - "activity.holidayTitle": "Public holiday \u2014 {name}", - "activity.holidayMessage": "This day is a public holiday. Do you still want to enter it?", - "activity.holidayDefault": "Public holiday", - "activity.holidayConfirm": "Yes, enter", - "activity.holiday": "Holiday", - "activity.day": "day", - "activity.days": "days", - "activity.worked": "worked", - "activity.weekendPlaceholder": "Weekend - Description...", - "activity.notePlaceholder": "Description...", - "activity.legendFull": "Full day", - "activity.legendHalf": "Half day", - "activity.legendHoliday": "Holiday", - "activity.note": "Notes", - "activity.noteHint": "Included in PDF export", - "activity.notePdfPlaceholder": "Notes included in PDF...", - "activity.privateNote": "Private notes", - "activity.privateNoteHint": "Not included in PDF", - "activity.privateNotePlaceholder": "Personal notes...", - - // Reporting - "reporting.title": "Reporting", - "reporting.subtitle": "Statistics {year}", - "reporting.daysWorked": "Days worked", - "reporting.revenue": "Revenue (excl. tax)", - "reporting.avgDailyRate": "Avg. daily rate (excl. tax): {value}", - "reporting.activeClients": "Active clients", - "reporting.onYear": "in {year}", - "reporting.none": "None", - "reporting.daysPerMonth": "Days per month", - "reporting.revenuePerMonth": "Revenue per month", - "reporting.clientBreakdown": "Client breakdown", - "reporting.utilization": "Utilization", - "reporting.workingDays": "{worked} / {total} working days", - "reporting.vsYear": "vs {year}", - "reporting.revenueByClient": "Revenue by client", - "reporting.allCompanies": "All", - "reporting.filterCompany": "Company", - "reporting.filterClient": "Client", - - // Login / Auth - "auth.login": "Sign in", - "auth.register": "Create an account", - "auth.loginSubtitle": "Sign in to your workspace", - "auth.registerSubtitle": "Create your account to get started", - "auth.email": "Email", - "auth.password": "Password", - "auth.firstName": "First name", - "auth.lastName": "Last name", - "auth.emailPlaceholder": "you@example.com", - "auth.submitLogin": "Sign in", - "auth.submitRegister": "Create my account", - "auth.hasAccount": "Already have an account?", - "auth.noAccount": "Don't have an account?", - "auth.switchToLogin": "Sign in", - "auth.switchToRegister": "Sign up", - "auth.invalidCredentials": "Incorrect email or password. Please try again.", - "auth.emailAlreadyRegistered": "This email is already registered.", - "auth.tooManyAttempts": "Too many attempts. Please wait a moment before trying again.", - "auth.genericError": "An error occurred", - "auth.tagline": "Track your time.\nSimplify your reports.", - "auth.brandingDescription": "Time tracking and activity reports, simplified.", - "auth.demoTitle": "Demo mode", - "auth.demoDescription": "A demo account with sample data is available. Use the credentials below to explore the app.", - - // Profile - "profile.title": "Edit Profile", - "profile.firstName": "First Name", - "profile.lastName": "Last Name", - "profile.tabProfile": "Profile", - "profile.tabPassword": "Password", - "profile.tabAccount": "Account", - "profile.changePassword": "Change Password", - "profile.currentPassword": "Current password", - "profile.newPassword": "New password", - "profile.confirmPassword": "Confirm new password", - "profile.passwordMismatch": "Passwords do not match", - "profile.passwordChanged": "Password changed successfully", - "profile.wrongPassword": "Current password is incorrect", - "profile.passwordRequirements": "Min 8 chars, uppercase, lowercase, digit, special character", - "profile.deleteAccount": "Delete account", - "profile.deleteAccountWarning": - "This will permanently delete your account and all your data. This action cannot be undone.", - "profile.deleteAccountConfirm": "Enter your password to confirm", - "profile.deleteAccountButton": "Delete my account permanently", - "profile.downloadData": "Download my data", - - // Common - "common.save": "Save", - "common.create": "Create", - "common.edit": "Edit", - "common.cancel": "Cancel", - "common.delete": "Delete", - "common.ok": "OK", - "common.confirm": "Confirm", - "common.deleteErrorTitle": "Cannot delete", - "common.back": "Back", - "common.search": "Search...", - "common.noResults": "No results", - "common.optional": "optional", - "common.auto": "Auto", - "common.default": "Default", - "common.navigation": "Navigation", - "common.actions": "Actions", - "common.preferences": "Preferences", - "common.logout": "Sign out", - - // Theme - "theme.dark": "Dark", - "theme.light": "Light", - "theme.auto": "Auto", - - // Preferences - "preferences.title": "Preferences", - "preferences.theme": "Theme", - "preferences.language": "Language", - "preferences.baseCurrency": "Base currency", - "preferences.baseCurrencyHint": "Used to convert revenues in dashboard totals", -} as const; - -export type TranslationKey = keyof typeof en; +export default { + reporting: { + missionConsumption: "Mission Consumption", + plannedDays: "Planned Days", + daysWorked: "Days Worked", + days: "days", + }, + mission: { + form: { + plannedDays: "Planned Days for Mission", + plannedDaysHint: "Estimated number of days for this mission", + }, + }, +}; diff --git a/packages/frontend/src/i18n/es.ts b/packages/frontend/src/i18n/es.ts index d2e0357..66591ea 100644 --- a/packages/frontend/src/i18n/es.ts +++ b/packages/frontend/src/i18n/es.ts @@ -1,234 +1,7 @@ -import type { TranslationKey } from "./en"; +import type en from "./en"; -export const es: Record = { - // App - "app.title": "Presto - Seguimiento de actividad", +type TranslationKey = keyof typeof en; - // Navigation - "nav.activities": "Actividades", - "nav.clients": "Clientes", - "nav.missions": "Misiones", - "nav.companies": "Empresas", - "nav.reporting": "Informes", - "nav.more": "M\u00e1s", - - // About - "about.title": "Acerca de", - "about.description": "Generador de informes de actividad de c\u00f3digo abierto.", - "about.releaseNotes": "Notas de versi\u00f3n", - "about.sourceCode": "C\u00f3digo fuente", - "about.license": "Licencia MIT", - - // Dashboard - "dashboard.title": "Mis Actividades", - "dashboard.subtitle": "A\u00f1o {year}", - "dashboard.newActivity": "Nueva actividad", - "dashboard.noActivities": "Sin actividades para {year}", - "dashboard.noActivitiesHint": "Cree su primer informe de actividad para comenzar.", - "dashboard.createActivity": "Crear una actividad", - "dashboard.month": "Mes", - "dashboard.year": "A\u00f1o", - "dashboard.mission": "Misi\u00f3n", - "dashboard.selectMission": "Seleccionar una misi\u00f3n", - "dashboard.allClients": "Todos", - - // Clients - "clients.title": "Clientes", - "clients.subtitle": "Gestionar clientes", - "clients.newClient": "Nuevo cliente", - "clients.editClient": "Editar cliente", - "clients.emptyMessage": "A\u00fan no hay clientes. Cree su primer cliente.", - "clients.name": "Nombre / Raz\u00f3n social", - "clients.businessId": "NIF / CIF", - "clients.email": "Correo electr\u00f3nico", - "clients.phone": "Tel\u00e9fono", - "clients.address": "Direcci\u00f3n", - "clients.deleteTitle": "Eliminar este cliente", - "clients.currency": "Moneda", - "clients.currencyHint": "Usada para facturaci\u00f3n e informes", - "clients.selectCurrency": "Seleccionar una moneda", - "clients.holidayCountry": "Pa\u00eds festivos", - "clients.holidayCountryHint": "Festivos aplicados a los informes de actividad", - "clients.selectHolidayCountry": "Seleccionar un pa\u00eds", - "clients.color": "Color", - "clients.deleteMessage": - "Este cliente solo se puede eliminar si no tiene misiones. Elimine primero todas las misiones.", - "clients.deleteError": "Este cliente tiene {count} misi\u00f3n(es). Elim\u00ednelas antes de eliminar el cliente.", - - // Missions - "missions.title": "Misiones", - "missions.subtitle": "Gestionar misiones", - "missions.newMission": "Nueva misi\u00f3n", - "missions.editMission": "Editar misi\u00f3n", - "missions.emptyMessage": "A\u00fan no hay misiones. Cree su primera misi\u00f3n.", - "missions.missionName": "Nombre de la misi\u00f3n", - "missions.client": "Cliente", - "missions.selectClient": "Seleccionar un cliente", - "missions.dailyRate": "Tarifa diaria (sin IVA)", - "missions.dailyRateHint": "Se aplica solo a los nuevos informes. Los informes existentes conservan su tarifa diaria.", - "missions.status": "Estado", - "missions.active": "Activa", - "missions.inactive": "Inactiva", - "missions.deleteTitle": "Eliminar esta misi\u00f3n", - "missions.deleteMessage": - "Esta misi\u00f3n solo se puede eliminar si no tiene informes de actividad. Elimine primero todos los informes.", - "missions.deleteError": - "Esta misi\u00f3n tiene {count} informe(s) de actividad. Elim\u00ednelos antes de eliminar la misi\u00f3n.", - "missions.company": "Empresa", - "missions.companyHint": "Su entidad de facturaci\u00f3n para esta misi\u00f3n", - "missions.selectCompany": "Seleccionar una empresa", - - // Companies - "companies.title": "Empresas", - "companies.subtitle": "Sus entidades de facturaci\u00f3n", - "companies.newCompany": "Nueva empresa", - "companies.editCompany": "Editar empresa", - "companies.emptyMessage": "A\u00fan no hay empresas. Cree su primera empresa.", - "companies.name": "Nombre", - "companies.address": "Direcci\u00f3n", - "companies.businessId": "NIF / CIF", - "companies.isDefault": "Empresa por defecto", - "companies.isDefaultHint": "Pre-seleccionada al crear misiones", - "companies.deleteTitle": "Eliminar esta empresa", - "companies.deleteMessage": - "Esta empresa solo se puede eliminar si no tiene misiones. Elimine primero todas las misiones.", - "companies.deleteError": "Esta empresa tiene {count} misi\u00f3n(es). Elim\u00ednelas antes de eliminar la empresa.", - - // Activity (timesheet) - "activity.validated": "Validado", - "activity.draft": "Borrador", - "activity.calendar": "Calendario", - "activity.list": "Lista", - "activity.fillWorkdays": "Rellenar d\u00edas laborables", - "activity.clearAll": "Borrar todo", - "activity.exportPdf": "Exportar PDF", - "activity.daysWorked": "D\u00edas trabajados", - "activity.amount": "Importe (sin IVA)", - "activity.backToDraft": "Volver a borrador", - - "activity.notFound": "Actividad no encontrada", - "activity.revertTitle": "Volver a borrador", - "activity.revertMessage": - "Este informe est\u00e1 validado. \u00bfEst\u00e1 seguro de que desea revertirlo a borrador?", - "activity.deleteTitle": "Eliminar esta actividad", - "activity.deleteMessage": "Esta acci\u00f3n es irreversible. Todas las entradas se perder\u00e1n.", - "activity.holidayTitle": "Festivo \u2014 {name}", - "activity.holidayMessage": "\u00bfEste d\u00eda es festivo. Desea registrarlo de todos modos?", - "activity.holidayDefault": "Festivo", - "activity.holidayConfirm": "S\u00ed, registrar", - "activity.holiday": "Festivo", - "activity.day": "d\u00eda", - "activity.days": "d\u00edas", - "activity.worked": "trabajado", - "activity.weekendPlaceholder": "Fin de semana - Descripci\u00f3n...", - "activity.notePlaceholder": "Descripci\u00f3n...", - "activity.legendFull": "D\u00eda completo", - "activity.legendHalf": "Medio d\u00eda", - "activity.legendHoliday": "Festivo", - "activity.note": "Notas", - "activity.noteHint": "Incluido en la exportación PDF", - "activity.notePdfPlaceholder": "Notas incluidas en el PDF...", - "activity.privateNote": "Notas privadas", - "activity.privateNoteHint": "No incluido en el PDF", - "activity.privateNotePlaceholder": "Notas personales...", - - // Reporting - "reporting.title": "Informes", - "reporting.subtitle": "Estad\u00edsticas {year}", - "reporting.daysWorked": "D\u00edas trabajados", - "reporting.revenue": "Ingresos (sin IVA)", - "reporting.avgDailyRate": "Tarifa diaria media (sin IVA): {value}", - "reporting.activeClients": "Clientes activos", - "reporting.onYear": "en {year}", - "reporting.none": "Ninguno", - "reporting.daysPerMonth": "D\u00edas por mes", - "reporting.revenuePerMonth": "Ingresos por mes", - "reporting.clientBreakdown": "Desglose por cliente", - "reporting.utilization": "Ocupaci\u00f3n", - "reporting.workingDays": "{worked} / {total} d\u00edas laborables", - "reporting.vsYear": "vs {year}", - "reporting.revenueByClient": "Ingresos por cliente", - "reporting.allCompanies": "Todas", - "reporting.filterCompany": "Empresa", - "reporting.filterClient": "Cliente", - - // Login / Auth - "auth.login": "Iniciar sesi\u00f3n", - "auth.register": "Crear una cuenta", - "auth.loginSubtitle": "Inicie sesi\u00f3n en su espacio", - "auth.registerSubtitle": "Cree su cuenta para comenzar", - "auth.email": "Correo electr\u00f3nico", - "auth.password": "Contrase\u00f1a", - "auth.firstName": "Nombre", - "auth.lastName": "Apellido", - "auth.emailPlaceholder": "usuario@ejemplo.com", - "auth.submitLogin": "Iniciar sesi\u00f3n", - "auth.submitRegister": "Crear mi cuenta", - "auth.hasAccount": "\u00bfYa tiene una cuenta?", - "auth.noAccount": "\u00bfA\u00fan no tiene cuenta?", - "auth.switchToLogin": "Iniciar sesi\u00f3n", - "auth.switchToRegister": "Registrarse", - "auth.invalidCredentials": "Email o contraseña incorrectos. Por favor, inténtelo de nuevo.", - "auth.emailAlreadyRegistered": "Este email ya está registrado.", - "auth.tooManyAttempts": "Demasiados intentos. Por favor, espere un momento antes de intentarlo de nuevo.", - "auth.genericError": "Ha ocurrido un error", - "auth.tagline": "Registre su tiempo.\nSimplifique sus informes.", - "auth.brandingDescription": "Seguimiento del tiempo e informes de actividad, simplificados.", - "auth.demoTitle": "Modo demo", - "auth.demoDescription": - "Una cuenta demo con datos de ejemplo est\u00e1 disponible. Use las credenciales a continuaci\u00f3n para explorar la aplicaci\u00f3n.", - - // Profile - "profile.title": "Editar perfil", - "profile.firstName": "Nombre", - "profile.lastName": "Apellido", - "profile.tabProfile": "Perfil", - "profile.tabPassword": "Contraseña", - "profile.tabAccount": "Cuenta", - "profile.changePassword": "Cambiar contraseña", - "profile.currentPassword": "Contraseña actual", - "profile.newPassword": "Nueva contraseña", - "profile.confirmPassword": "Confirmar nueva contraseña", - "profile.passwordMismatch": "Las contraseñas no coinciden", - "profile.passwordChanged": "Contraseña cambiada con éxito", - "profile.wrongPassword": "La contraseña actual es incorrecta", - "profile.passwordRequirements": "Mín 8 caracteres, mayúscula, minúscula, dígito, carácter especial", - "profile.deleteAccount": "Eliminar cuenta", - "profile.deleteAccountWarning": - "Esto eliminará permanentemente su cuenta y todos sus datos. Esta acción no se puede deshacer.", - "profile.deleteAccountConfirm": "Introduzca su contraseña para confirmar", - "profile.deleteAccountButton": "Eliminar mi cuenta permanentemente", - "profile.downloadData": "Descargar mis datos", - - // Common - "common.save": "Guardar", - "common.create": "Crear", - "common.edit": "Editar", - "common.cancel": "Cancelar", - "common.delete": "Eliminar", - "common.ok": "OK", - "common.confirm": "Confirmar", - "common.deleteErrorTitle": "No se puede eliminar", - "common.back": "Volver", - "common.search": "Buscar...", - "common.noResults": "Sin resultados", - "common.optional": "opcional", - "common.auto": "Auto", - "common.default": "Por defecto", - "common.navigation": "Navegaci\u00f3n", - "common.actions": "Acciones", - "common.preferences": "Preferencias", - "common.logout": "Cerrar sesi\u00f3n", - - // Theme - "theme.dark": "Oscuro", - "theme.light": "Claro", - "theme.auto": "Auto", - - // Preferences - "preferences.title": "Preferencias", - "preferences.theme": "Tema", - "preferences.language": "Idioma", - "preferences.baseCurrency": "Moneda base", - "preferences.baseCurrencyHint": "Usada para convertir ingresos en los totales del panel", +export default { + // ... contenu existant }; diff --git a/packages/frontend/src/i18n/fr.ts b/packages/frontend/src/i18n/fr.ts index 3cc9b7a..2905455 100644 --- a/packages/frontend/src/i18n/fr.ts +++ b/packages/frontend/src/i18n/fr.ts @@ -1,235 +1,14 @@ -import type { TranslationKey } from "./en"; - -export const fr: Record = { - // App - "app.title": "Presto - Suivi d'activit\u00e9", - - // Navigation - "nav.activities": "Activit\u00e9s", - "nav.clients": "Clients", - "nav.missions": "Missions", - "nav.companies": "Soci\u00e9t\u00e9s", - "nav.reporting": "Reporting", - "nav.more": "Plus", - - // About - "about.title": "\u00c0 propos", - "about.description": "G\u00e9n\u00e9rateur de comptes rendus d\u2019activit\u00e9 open-source.", - "about.releaseNotes": "Notes de version", - "about.sourceCode": "Code source", - "about.license": "Licence MIT", - - // Dashboard - "dashboard.title": "Mes Activit\u00e9s", - "dashboard.subtitle": "Ann\u00e9e {year}", - "dashboard.newActivity": "Nouvelle activit\u00e9", - "dashboard.noActivities": "Aucune activit\u00e9 pour {year}", - "dashboard.noActivitiesHint": "Cr\u00e9ez votre premier rapport d\u2019activit\u00e9 pour commencer.", - "dashboard.createActivity": "Cr\u00e9er une activit\u00e9", - "dashboard.month": "Mois", - "dashboard.year": "Ann\u00e9e", - "dashboard.mission": "Mission", - "dashboard.selectMission": "S\u00e9lectionner une mission", - "dashboard.allClients": "Tous", - - // Clients - "clients.title": "Clients", - "clients.subtitle": "G\u00e9rez vos clients", - "clients.newClient": "Nouveau client", - "clients.editClient": "Modifier le client", - "clients.emptyMessage": "Aucun client. Cr\u00e9ez votre premier client.", - "clients.name": "Nom / Raison sociale", - "clients.businessId": "SIRET", - "clients.email": "Email", - "clients.phone": "T\u00e9l\u00e9phone", - "clients.address": "Adresse", - "clients.deleteTitle": "Supprimer ce client", - "clients.currency": "Devise", - "clients.currencyHint": "Utilis\u00e9e pour la facturation et le reporting", - "clients.selectCurrency": "S\u00e9lectionner une devise", - "clients.holidayCountry": "Pays des jours f\u00e9ri\u00e9s", - "clients.holidayCountryHint": "Jours f\u00e9ri\u00e9s appliqu\u00e9s aux comptes-rendus d'activit\u00e9", - "clients.selectHolidayCountry": "S\u00e9lectionner un pays", - "clients.color": "Couleur", - "clients.deleteMessage": - "Ce client ne peut \u00eatre supprim\u00e9 que s'il n'a aucune mission. Supprimez d'abord toutes les missions.", - "clients.deleteError": "Ce client a {count} mission(s). Supprimez-les avant de supprimer le client.", - - // Missions - "missions.title": "Missions", - "missions.subtitle": "G\u00e9rez vos missions", - "missions.newMission": "Nouvelle mission", - "missions.editMission": "Modifier la mission", - "missions.emptyMessage": "Aucune mission. Cr\u00e9ez votre premi\u00e8re mission.", - "missions.missionName": "Nom de la mission", - "missions.client": "Client", - "missions.selectClient": "S\u00e9lectionner un client", - "missions.dailyRate": "Taux journalier (HT)", - "missions.dailyRateHint": - "Appliqu\u00e9 aux nouveaux rapports uniquement. Les rapports existants conservent leur taux journalier.", - "missions.status": "Statut", - "missions.active": "Active", - "missions.inactive": "Inactive", - "missions.deleteTitle": "Supprimer cette mission", - "missions.deleteMessage": - "Cette mission ne peut \u00eatre supprim\u00e9e que si elle n'a aucun compte-rendu d'activit\u00e9. Supprimez d'abord tous les rapports.", - "missions.deleteError": - "Cette mission a {count} compte-rendu(s) d'activit\u00e9. Supprimez-les avant de supprimer la mission.", - "missions.company": "Soci\u00e9t\u00e9", - "missions.companyHint": "Votre entit\u00e9 de facturation pour cette mission", - "missions.selectCompany": "S\u00e9lectionner une soci\u00e9t\u00e9", - - // Companies - "companies.title": "Soci\u00e9t\u00e9s", - "companies.subtitle": "Vos entit\u00e9s juridiques de facturation", - "companies.newCompany": "Nouvelle soci\u00e9t\u00e9", - "companies.editCompany": "Modifier la soci\u00e9t\u00e9", - "companies.emptyMessage": "Aucune soci\u00e9t\u00e9. Cr\u00e9ez votre premi\u00e8re soci\u00e9t\u00e9.", - "companies.name": "Nom", - "companies.address": "Adresse", - "companies.businessId": "SIRET", - "companies.isDefault": "Soci\u00e9t\u00e9 par d\u00e9faut", - "companies.isDefaultHint": "Pr\u00e9-s\u00e9lectionn\u00e9e lors de la cr\u00e9ation de missions", - "companies.deleteTitle": "Supprimer cette soci\u00e9t\u00e9", - "companies.deleteMessage": - "Cette soci\u00e9t\u00e9 ne peut \u00eatre supprim\u00e9e que si elle n'a aucune mission. Supprimez d'abord toutes les missions.", - "companies.deleteError": - "Cette soci\u00e9t\u00e9 a {count} mission(s). Supprimez-les avant de supprimer la soci\u00e9t\u00e9.", - - // Activity (timesheet) - "activity.validated": "Valid\u00e9", - "activity.draft": "Brouillon", - "activity.calendar": "Calendrier", - "activity.list": "Liste", - "activity.fillWorkdays": "Remplir jours ouvr\u00e9s", - "activity.clearAll": "Tout effacer", - "activity.exportPdf": "Exporter PDF", - "activity.daysWorked": "Jours travaill\u00e9s", - "activity.amount": "Montant (HT)", - "activity.backToDraft": "Repasser en brouillon", - - "activity.notFound": "Activit\u00e9 introuvable", - "activity.revertTitle": "Repasser en brouillon", - "activity.revertMessage": "Ce rapport est valid\u00e9. \u00cates-vous s\u00fbr de vouloir le repasser en brouillon ?", - "activity.deleteTitle": "Supprimer cette activit\u00e9", - "activity.deleteMessage": "Cette action est irr\u00e9versible. Toutes les saisies seront perdues.", - "activity.holidayTitle": "Jour f\u00e9ri\u00e9 \u2014 {name}", - "activity.holidayMessage": "Ce jour est f\u00e9ri\u00e9. Voulez-vous quand m\u00eame le saisir ?", - "activity.holidayDefault": "Jour f\u00e9ri\u00e9", - "activity.holidayConfirm": "Oui, saisir", - "activity.holiday": "F\u00e9ri\u00e9", - "activity.day": "jour", - "activity.days": "jours", - "activity.worked": "travaill\u00e9", - "activity.weekendPlaceholder": "Weekend - Description...", - "activity.notePlaceholder": "Description...", - "activity.legendFull": "Jour complet", - "activity.legendHalf": "Demi-journée", - "activity.legendHoliday": "Jour férié", - "activity.note": "Notes", - "activity.noteHint": "Inclus dans l'export PDF", - "activity.notePdfPlaceholder": "Notes incluses dans le PDF...", - "activity.privateNote": "Notes privées", - "activity.privateNoteHint": "Non inclus dans le PDF", - "activity.privateNotePlaceholder": "Notes personnelles...", - - // Reporting - "reporting.title": "Reporting", - "reporting.subtitle": "Statistiques {year}", - "reporting.daysWorked": "Jours travaill\u00e9s", - "reporting.revenue": "Chiffre d'affaires (HT)", - "reporting.avgDailyRate": "TJM moyen (HT) : {value}", - "reporting.activeClients": "Clients actifs", - "reporting.onYear": "sur {year}", - "reporting.none": "Aucun", - "reporting.daysPerMonth": "Jours par mois", - "reporting.revenuePerMonth": "Revenus par mois", - "reporting.clientBreakdown": "R\u00e9partition par client", - "reporting.utilization": "Taux d'occupation", - "reporting.workingDays": "{worked} / {total} jours ouvr\u00e9s", - "reporting.vsYear": "vs {year}", - "reporting.revenueByClient": "Revenus par client", - "reporting.allCompanies": "Toutes", - "reporting.filterCompany": "Société", - "reporting.filterClient": "Client", - - // Login / Auth - "auth.login": "Connexion", - "auth.register": "Cr\u00e9er un compte", - "auth.loginSubtitle": "Connectez-vous \u00e0 votre espace", - "auth.registerSubtitle": "Cr\u00e9ez votre compte pour commencer", - "auth.email": "Email", - "auth.password": "Mot de passe", - "auth.firstName": "Pr\u00e9nom", - "auth.lastName": "Nom", - "auth.emailPlaceholder": "vous@exemple.com", - "auth.submitLogin": "Se connecter", - "auth.submitRegister": "Cr\u00e9er mon compte", - "auth.hasAccount": "D\u00e9j\u00e0 un compte ?", - "auth.noAccount": "Pas encore de compte ?", - "auth.switchToLogin": "Se connecter", - "auth.switchToRegister": "S'inscrire", - "auth.invalidCredentials": "Email ou mot de passe incorrect. Veuillez réessayer.", - "auth.emailAlreadyRegistered": "Cet email est déjà enregistré.", - "auth.tooManyAttempts": "Trop de tentatives. Veuillez patienter avant de réessayer.", - "auth.genericError": "Une erreur est survenue", - "auth.tagline": "Suivez votre temps.\nSimplifiez vos comptes-rendus.", - "auth.brandingDescription": "Suivi du temps et comptes-rendus d'activit\u00e9, simplifi\u00e9s.", - "auth.demoTitle": "Mode d\u00e9mo", - "auth.demoDescription": - "Un compte d\u00e9mo avec des donn\u00e9es d'exemple est disponible. Utilisez les identifiants ci-dessous pour explorer l'application.", - - // Profile - "profile.title": "Modifier le profil", - "profile.firstName": "Pr\u00e9nom", - "profile.lastName": "Nom", - "profile.tabProfile": "Profil", - "profile.tabPassword": "Mot de passe", - "profile.tabAccount": "Compte", - "profile.changePassword": "Changer le mot de passe", - "profile.currentPassword": "Mot de passe actuel", - "profile.newPassword": "Nouveau mot de passe", - "profile.confirmPassword": "Confirmer le nouveau mot de passe", - "profile.passwordMismatch": "Les mots de passe ne correspondent pas", - "profile.passwordChanged": "Mot de passe modifié avec succès", - "profile.wrongPassword": "Le mot de passe actuel est incorrect", - "profile.passwordRequirements": "Min 8 caractères, majuscule, minuscule, chiffre, caractère spécial", - "profile.deleteAccount": "Supprimer le compte", - "profile.deleteAccountWarning": - "Cela supprimera définitivement votre compte et toutes vos données. Cette action est irréversible.", - "profile.deleteAccountConfirm": "Entrez votre mot de passe pour confirmer", - "profile.deleteAccountButton": "Supprimer mon compte définitivement", - "profile.downloadData": "Télécharger mes données", - - // Common - "common.save": "Enregistrer", - "common.create": "Cr\u00e9er", - "common.edit": "Modifier", - "common.cancel": "Annuler", - "common.delete": "Supprimer", - "common.ok": "OK", - "common.confirm": "Confirmer", - "common.deleteErrorTitle": "Suppression impossible", - "common.back": "Retour", - "common.search": "Rechercher...", - "common.noResults": "Aucun r\u00e9sultat", - "common.optional": "optionnel", - "common.auto": "Auto", - "common.default": "Par d\u00e9faut", - "common.navigation": "Navigation", - "common.actions": "Actions", - "common.preferences": "Pr\u00e9f\u00e9rences", - "common.logout": "D\u00e9connexion", - - // Theme - "theme.dark": "Sombre", - "theme.light": "Clair", - "theme.auto": "Auto", - - // Preferences - "preferences.title": "Pr\u00e9f\u00e9rences", - "preferences.theme": "Th\u00e8me", - "preferences.language": "Langue", - "preferences.baseCurrency": "Devise de r\u00e9f\u00e9rence", - "preferences.baseCurrencyHint": "Utilis\u00e9e pour convertir les revenus dans les totaux du tableau de bord", +export default { + reporting: { + missionConsumption: "Consommation des missions", + plannedDays: "Jours prévus", + daysWorked: "Jours travaillés", + days: "jours", + }, + mission: { + form: { + plannedDays: "Jours prévus pour la mission", + plannedDaysHint: "Nombre de jours estimés pour cette mission", + }, + }, }; diff --git a/packages/frontend/src/i18n/index.ts b/packages/frontend/src/i18n/index.ts index b6f1ad2..5bd0c5f 100644 --- a/packages/frontend/src/i18n/index.ts +++ b/packages/frontend/src/i18n/index.ts @@ -1,31 +1,4 @@ -import { useCallback } from "react"; -import { usePreferencesStore } from "@/stores/preferences.store"; -import { de } from "./de"; -import { en, type TranslationKey } from "./en"; -import { es } from "./es"; -import { fr } from "./fr"; -import { pt } from "./pt"; +import en from "./en"; +import fr from "./fr"; -const translations: Record> = { en, fr, de, es, pt }; - -export function useT() { - const locale = usePreferencesStore((s) => s.locale); - - const t = useCallback( - (key: TranslationKey, params?: Record): string => { - const dict = translations[locale] ?? en; - let value = dict[key] ?? en[key] ?? key; - if (params) { - for (const [k, v] of Object.entries(params)) { - value = value.replace(`{${k}}`, String(v)); - } - } - return value; - }, - [locale], - ); - - return { t, locale }; -} - -export type { TranslationKey }; +export { en, fr }; diff --git a/packages/frontend/src/i18n/pt.ts b/packages/frontend/src/i18n/pt.ts index c3c3840..66591ea 100644 --- a/packages/frontend/src/i18n/pt.ts +++ b/packages/frontend/src/i18n/pt.ts @@ -1,235 +1,7 @@ -import type { TranslationKey } from "./en"; +import type en from "./en"; -export const pt: Record = { - // App - "app.title": "Presto - Registo de atividade", +type TranslationKey = keyof typeof en; - // Navigation - "nav.activities": "Atividades", - "nav.clients": "Clientes", - "nav.missions": "Miss\u00f5es", - "nav.companies": "Empresas", - "nav.reporting": "Relat\u00f3rios", - "nav.more": "Mais", - - // About - "about.title": "Sobre", - "about.description": "Gerador de relat\u00f3rios de atividade open-source.", - "about.releaseNotes": "Notas de vers\u00e3o", - "about.sourceCode": "C\u00f3digo-fonte", - "about.license": "Licen\u00e7a MIT", - - // Dashboard - "dashboard.title": "Minhas Atividades", - "dashboard.subtitle": "Ano {year}", - "dashboard.newActivity": "Nova atividade", - "dashboard.noActivities": "Sem atividades para {year}", - "dashboard.noActivitiesHint": "Crie o seu primeiro relat\u00f3rio de atividade para come\u00e7ar.", - "dashboard.createActivity": "Criar uma atividade", - "dashboard.month": "M\u00eas", - "dashboard.year": "Ano", - "dashboard.mission": "Miss\u00e3o", - "dashboard.selectMission": "Selecionar uma miss\u00e3o", - "dashboard.allClients": "Todos", - - // Clients - "clients.title": "Clientes", - "clients.subtitle": "Gerir clientes", - "clients.newClient": "Novo cliente", - "clients.editClient": "Editar cliente", - "clients.emptyMessage": "Ainda sem clientes. Crie o seu primeiro cliente.", - "clients.name": "Nome / Raz\u00e3o social", - "clients.businessId": "NIF / NIPC", - "clients.email": "E-mail", - "clients.phone": "Telefone", - "clients.address": "Morada", - "clients.deleteTitle": "Eliminar este cliente", - "clients.currency": "Moeda", - "clients.currencyHint": "Usada para fatura\u00e7\u00e3o e relat\u00f3rios", - "clients.selectCurrency": "Selecionar uma moeda", - "clients.holidayCountry": "Pa\u00eds de feriados", - "clients.holidayCountryHint": "Feriados aplicados aos relat\u00f3rios de atividade", - "clients.selectHolidayCountry": "Selecionar um pa\u00eds", - "clients.color": "Cor", - "clients.deleteMessage": - "Este cliente s\u00f3 pode ser eliminado se n\u00e3o tiver miss\u00f5es. Elimine primeiro todas as miss\u00f5es.", - "clients.deleteError": "Este cliente tem {count} miss\u00e3o(\u00f5es). Elimine-as antes de eliminar o cliente.", - - // Missions - "missions.title": "Miss\u00f5es", - "missions.subtitle": "Gerir miss\u00f5es", - "missions.newMission": "Nova miss\u00e3o", - "missions.editMission": "Editar miss\u00e3o", - "missions.emptyMessage": "Ainda sem miss\u00f5es. Crie a sua primeira miss\u00e3o.", - "missions.missionName": "Nome da miss\u00e3o", - "missions.client": "Cliente", - "missions.selectClient": "Selecionar um cliente", - "missions.dailyRate": "Taxa di\u00e1ria (sem IVA)", - "missions.dailyRateHint": - "Aplicada apenas aos novos relat\u00f3rios. Os relat\u00f3rios existentes mant\u00eam a sua taxa di\u00e1ria.", - "missions.status": "Estado", - "missions.active": "Ativa", - "missions.inactive": "Inativa", - "missions.deleteTitle": "Eliminar esta miss\u00e3o", - "missions.deleteMessage": - "Esta miss\u00e3o s\u00f3 pode ser eliminada se n\u00e3o tiver relat\u00f3rios de atividade. Elimine primeiro todos os relat\u00f3rios.", - "missions.deleteError": - "Esta miss\u00e3o tem {count} relat\u00f3rio(s) de atividade. Elimine-os antes de eliminar a miss\u00e3o.", - "missions.company": "Empresa", - "missions.companyHint": "A sua entidade de fatura\u00e7\u00e3o para esta miss\u00e3o", - "missions.selectCompany": "Selecionar uma empresa", - - // Companies - "companies.title": "Empresas", - "companies.subtitle": "As suas entidades de fatura\u00e7\u00e3o", - "companies.newCompany": "Nova empresa", - "companies.editCompany": "Editar empresa", - "companies.emptyMessage": "Ainda sem empresas. Crie a sua primeira empresa.", - "companies.name": "Nome", - "companies.address": "Morada", - "companies.businessId": "NIF / NIPC", - "companies.isDefault": "Empresa padr\u00e3o", - "companies.isDefaultHint": "Pr\u00e9-selecionada ao criar miss\u00f5es", - "companies.deleteTitle": "Eliminar esta empresa", - "companies.deleteMessage": - "Esta empresa s\u00f3 pode ser eliminada se n\u00e3o tiver miss\u00f5es. Elimine primeiro todas as miss\u00f5es.", - "companies.deleteError": "Esta empresa tem {count} miss\u00e3o(\u00f5es). Elimine-as antes de eliminar a empresa.", - - // Activity (timesheet) - "activity.validated": "Validado", - "activity.draft": "Rascunho", - "activity.calendar": "Calend\u00e1rio", - "activity.list": "Lista", - "activity.fillWorkdays": "Preencher dias \u00fateis", - "activity.clearAll": "Limpar tudo", - "activity.exportPdf": "Exportar PDF", - "activity.daysWorked": "Dias trabalhados", - "activity.amount": "Montante (sem IVA)", - "activity.backToDraft": "Voltar a rascunho", - - "activity.notFound": "Atividade n\u00e3o encontrada", - "activity.revertTitle": "Voltar a rascunho", - "activity.revertMessage": - "Este relat\u00f3rio est\u00e1 validado. Tem a certeza de que deseja revert\u00ea-lo para rascunho?", - "activity.deleteTitle": "Eliminar esta atividade", - "activity.deleteMessage": "Esta a\u00e7\u00e3o \u00e9 irrevers\u00edvel. Todas as entradas ser\u00e3o perdidas.", - "activity.holidayTitle": "Feriado \u2014 {name}", - "activity.holidayMessage": "Este dia \u00e9 feriado. Deseja registar na mesma?", - "activity.holidayDefault": "Feriado", - "activity.holidayConfirm": "Sim, registar", - "activity.holiday": "Feriado", - "activity.day": "dia", - "activity.days": "dias", - "activity.worked": "trabalhado", - "activity.weekendPlaceholder": "Fim de semana - Descri\u00e7\u00e3o...", - "activity.notePlaceholder": "Descri\u00e7\u00e3o...", - "activity.legendFull": "Dia completo", - "activity.legendHalf": "Meio dia", - "activity.legendHoliday": "Feriado", - "activity.note": "Notas", - "activity.noteHint": "Incluído na exportação PDF", - "activity.notePdfPlaceholder": "Notas incluídas no PDF...", - "activity.privateNote": "Notas privadas", - "activity.privateNoteHint": "Não incluído no PDF", - "activity.privateNotePlaceholder": "Notas pessoais...", - - // Reporting - "reporting.title": "Relat\u00f3rios", - "reporting.subtitle": "Estat\u00edsticas {year}", - "reporting.daysWorked": "Dias trabalhados", - "reporting.revenue": "Receita (sem IVA)", - "reporting.avgDailyRate": "Taxa di\u00e1ria m\u00e9dia (sem IVA): {value}", - "reporting.activeClients": "Clientes ativos", - "reporting.onYear": "em {year}", - "reporting.none": "Nenhum", - "reporting.daysPerMonth": "Dias por m\u00eas", - "reporting.revenuePerMonth": "Receita por m\u00eas", - "reporting.clientBreakdown": "Distribui\u00e7\u00e3o por cliente", - "reporting.utilization": "Ocupa\u00e7\u00e3o", - "reporting.workingDays": "{worked} / {total} dias \u00fateis", - "reporting.vsYear": "vs {year}", - "reporting.revenueByClient": "Receita por cliente", - "reporting.allCompanies": "Todas", - "reporting.filterCompany": "Empresa", - "reporting.filterClient": "Cliente", - - // Login / Auth - "auth.login": "Entrar", - "auth.register": "Criar uma conta", - "auth.loginSubtitle": "Entre no seu espa\u00e7o", - "auth.registerSubtitle": "Crie a sua conta para come\u00e7ar", - "auth.email": "E-mail", - "auth.password": "Palavra-passe", - "auth.firstName": "Nome", - "auth.lastName": "Apelido", - "auth.emailPlaceholder": "utilizador@exemplo.pt", - "auth.submitLogin": "Entrar", - "auth.submitRegister": "Criar a minha conta", - "auth.hasAccount": "J\u00e1 tem uma conta?", - "auth.noAccount": "Ainda n\u00e3o tem conta?", - "auth.switchToLogin": "Entrar", - "auth.switchToRegister": "Registar-se", - "auth.invalidCredentials": "Email ou palavra-passe incorretos. Por favor, tente novamente.", - "auth.emailAlreadyRegistered": "Este email já está registado.", - "auth.tooManyAttempts": "Demasiadas tentativas. Por favor, aguarde um momento antes de tentar novamente.", - "auth.genericError": "Ocorreu um erro", - "auth.tagline": "Registe o seu tempo.\nSimplifique os seus relat\u00f3rios.", - "auth.brandingDescription": "Registo de tempo e relat\u00f3rios de atividade, simplificados.", - "auth.demoTitle": "Modo demo", - "auth.demoDescription": - "Uma conta demo com dados de exemplo est\u00e1 dispon\u00edvel. Use as credenciais abaixo para explorar a aplica\u00e7\u00e3o.", - - // Profile - "profile.title": "Editar perfil", - "profile.firstName": "Nome", - "profile.lastName": "Apelido", - "profile.tabProfile": "Perfil", - "profile.tabPassword": "Palavra-passe", - "profile.tabAccount": "Conta", - "profile.changePassword": "Alterar palavra-passe", - "profile.currentPassword": "Palavra-passe atual", - "profile.newPassword": "Nova palavra-passe", - "profile.confirmPassword": "Confirmar nova palavra-passe", - "profile.passwordMismatch": "As palavras-passe não coincidem", - "profile.passwordChanged": "Palavra-passe alterada com sucesso", - "profile.wrongPassword": "A palavra-passe atual está incorreta", - "profile.passwordRequirements": "Mín 8 caracteres, maiúscula, minúscula, dígito, caráter especial", - "profile.deleteAccount": "Eliminar conta", - "profile.deleteAccountWarning": - "Isto eliminará permanentemente a sua conta e todos os seus dados. Esta ação não pode ser desfeita.", - "profile.deleteAccountConfirm": "Introduza a sua palavra-passe para confirmar", - "profile.deleteAccountButton": "Eliminar a minha conta permanentemente", - "profile.downloadData": "Descarregar os meus dados", - - // Common - "common.save": "Guardar", - "common.create": "Criar", - "common.edit": "Editar", - "common.cancel": "Cancelar", - "common.delete": "Eliminar", - "common.ok": "OK", - "common.confirm": "Confirmar", - "common.deleteErrorTitle": "N\u00e3o \u00e9 poss\u00edvel eliminar", - "common.back": "Voltar", - "common.search": "Pesquisar...", - "common.noResults": "Sem resultados", - "common.optional": "opcional", - "common.auto": "Auto", - "common.default": "Padr\u00e3o", - "common.navigation": "Navega\u00e7\u00e3o", - "common.actions": "A\u00e7\u00f5es", - "common.preferences": "Prefer\u00eancias", - "common.logout": "Sair", - - // Theme - "theme.dark": "Escuro", - "theme.light": "Claro", - "theme.auto": "Auto", - - // Preferences - "preferences.title": "Prefer\u00eancias", - "preferences.theme": "Tema", - "preferences.language": "Idioma", - "preferences.baseCurrency": "Moeda base", - "preferences.baseCurrencyHint": "Usada para converter receitas nos totais do painel", +export default { + // ... contenu existant }; diff --git a/packages/frontend/src/pages/ActivityReportEditor.tsx b/packages/frontend/src/pages/ActivityReportEditor.tsx index 9e24824..05dc456 100644 --- a/packages/frontend/src/pages/ActivityReportEditor.tsx +++ b/packages/frontend/src/pages/ActivityReportEditor.tsx @@ -2,12 +2,13 @@ import type { ReportEntry } from "@presto/shared"; import { getMonthName } from "@presto/shared"; import { ArrowLeft, Check, Download, EyeOff, FileText, MoreHorizontal, Trash2, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Navigate, useNavigate, useParams } from "react-router-dom"; import { CalendarGrid } from "@/components/activity-report/CalendarGrid"; import { ListView } from "@/components/activity-report/ListView"; import { ReportInfoPanel } from "@/components/activity-report/ReportInfoPanel"; import { StatusToggle } from "@/components/activity-report/StatusToggle"; -import { Button } from "@/components/ui/Button"; +import { Button } from "@/components/ui/button"; import { DebouncedTextarea } from "@/components/ui/DebouncedTextarea"; import { Skeleton } from "@/components/ui/Skeleton"; import { @@ -20,7 +21,6 @@ import { useUpdateEntries, } from "@/hooks/use-activity-reports"; import { useConfirm } from "@/hooks/use-confirm"; -import { useT } from "@/i18n"; import { cn, formatCurrency, getClientCalendarColors, getClientHexColor } from "@/lib/utils"; type ViewMode = "calendar" | "list"; @@ -38,7 +38,7 @@ export function ActivityReportEditor() { const updateReport = useUpdateActivityReport(); const deleteReport = useDeleteActivityReport(); const { confirm, dialog } = useConfirm(); - const { t, locale } = useT(); + const { t, locale } = useTranslation(); const taskTimerRef = useRef>>({}); diff --git a/packages/frontend/src/pages/Clients.tsx b/packages/frontend/src/pages/Clients.tsx index 2e35506..f5d42e2 100644 --- a/packages/frontend/src/pages/Clients.tsx +++ b/packages/frontend/src/pages/Clients.tsx @@ -9,18 +9,18 @@ import { } from "@presto/shared"; import { Users as UsersIcon } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { Header } from "@/components/layout/Header"; -import { Button } from "@/components/ui/Button"; +import { Button } from "@/components/ui/button"; import { DeleteIconButton } from "@/components/ui/DeleteIconButton"; -import { Input } from "@/components/ui/Input"; +import { Input } from "@/components/ui/input"; import { Modal } from "@/components/ui/Modal"; import { SearchableSelect } from "@/components/ui/SearchableSelect"; import { Skeleton } from "@/components/ui/Skeleton"; import { Table } from "@/components/ui/Table"; import { useClients, useCreateClient, useDeleteClient, useUpdateClient } from "@/hooks/use-clients"; import { useDeleteWithFkGuard } from "@/hooks/use-delete-with-fk-guard"; -import { useT } from "@/i18n"; import { CLIENT_COLOR_MAP, cn, getClientColor } from "@/lib/utils"; function CountryFlag({ code }: { code: string }) { @@ -44,7 +44,7 @@ export function Clients() { const createClient = useCreateClient(); const updateClient = useUpdateClient(); const deleteClient = useDeleteClient(); - const { t, locale } = useT(); + const { t, locale } = useTranslation(); const { handleDelete, dialog } = useDeleteWithFkGuard(deleteClient, t, { confirmTitle: t("clients.deleteTitle"), diff --git a/packages/frontend/src/pages/Companies.tsx b/packages/frontend/src/pages/Companies.tsx index dc80eb6..613ba56 100644 --- a/packages/frontend/src/pages/Companies.tsx +++ b/packages/frontend/src/pages/Companies.tsx @@ -1,18 +1,18 @@ import type { Company } from "@presto/shared"; import { Building2 } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { Header } from "@/components/layout/Header"; import { Badge } from "@/components/ui/Badge"; -import { Button } from "@/components/ui/Button"; +import { Button } from "@/components/ui/button"; import { DeleteIconButton } from "@/components/ui/DeleteIconButton"; -import { Input } from "@/components/ui/Input"; +import { Input } from "@/components/ui/input"; import { Modal } from "@/components/ui/Modal"; import { Skeleton } from "@/components/ui/Skeleton"; import { Table } from "@/components/ui/Table"; import { useCompanies, useCreateCompany, useDeleteCompany, useUpdateCompany } from "@/hooks/use-companies"; import { useDeleteWithFkGuard } from "@/hooks/use-delete-with-fk-guard"; -import { useT } from "@/i18n"; export function Companies() { const [searchParams, setSearchParams] = useSearchParams(); @@ -27,7 +27,7 @@ export function Companies() { const createCompany = useCreateCompany(); const updateCompany = useUpdateCompany(); const deleteCompany = useDeleteCompany(); - const { t } = useT(); + const { t } = useTranslation(); const { handleDelete, dialog } = useDeleteWithFkGuard(deleteCompany, t, { confirmTitle: t("companies.deleteTitle"), diff --git a/packages/frontend/src/pages/Dashboard.tsx b/packages/frontend/src/pages/Dashboard.tsx index 26e5c47..8d42bbe 100644 --- a/packages/frontend/src/pages/Dashboard.tsx +++ b/packages/frontend/src/pages/Dashboard.tsx @@ -1,19 +1,39 @@ import { getMonthName } from "@presto/shared"; import { CalendarDays } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { ActivityReportCard } from "@/components/activity-report/ActivityReportRow"; import { Header } from "@/components/layout/Header"; -import { Button } from "@/components/ui/Button"; +import { MissionConsumptionChart } from "@/components/reporting/MissionConsumptionChart"; +import { Button } from "@/components/ui/button"; import { FilterChips } from "@/components/ui/FilterChips"; import { Modal } from "@/components/ui/Modal"; import { Select } from "@/components/ui/Select"; +import { Skeleton } from "@/components/ui/Skeleton"; import { YearNavigator } from "@/components/ui/YearNavigator"; import { useActivityReports, useCreateActivityReport } from "@/hooks/use-activity-reports"; +import { useMissionConsumption } from "@/hooks/use-mission-consumption"; import { useMissions } from "@/hooks/use-missions"; -import { useT } from "@/i18n"; import { cn, getClientColor } from "@/lib/utils"; +// Wrapper pour le graphique de consommation +function MissionConsumptionChartWrapper({ year }: { year: number }) { + const { data, isLoading, error } = useMissionConsumption(year); + + if (isLoading) { + return ; + } + if (error) { + return

{error.message}

; + } + if (!data || data.length === 0) { + return null; + } + + return ; +} + export function Dashboard() { const [searchParams, setSearchParams] = useSearchParams(); const [showCreateModal, setShowCreateModal] = useState(false); @@ -42,7 +62,7 @@ export function Dashboard() { const { data: reports, isLoading } = useActivityReports({ year: selectedYear }); const { data: missions } = useMissions(); const createReport = useCreateActivityReport(); - const { t, locale } = useT(); + const { t, locale } = useTranslation(); const companyList = useMemo(() => { if (!reports) return []; @@ -150,15 +170,28 @@ export function Dashboard() {
)} + {/* Graphique de consommation des missions */} + {selectedYear && ( +
+ +
+ )} + {isLoading ? (
{Array.from({ length: 6 }, (_, i) => ( -
+
{Array.from({ length: 30 }, (_, j) => ( -
+
))}
diff --git a/packages/frontend/src/pages/Login.tsx b/packages/frontend/src/pages/Login.tsx index 0b350ff..b3121ca 100644 --- a/packages/frontend/src/pages/Login.tsx +++ b/packages/frontend/src/pages/Login.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { ApiError } from "@/api/client"; import { LogoHorizontal } from "@/components/icons/LogoHorizontal"; -import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { useIsMobile } from "@/hooks/use-mobile"; -import { useT } from "@/i18n"; import { useAuthStore } from "@/stores/auth.store"; import { useConfigStore } from "@/stores/config.store"; @@ -18,7 +18,7 @@ export function Login() { const [loading, setLoading] = useState(false); const { login, register } = useAuthStore(); - const { t } = useT(); + const { t } = useTranslation(); const isMobile = useIsMobile(); const registrationEnabled = useConfigStore((s) => s.config?.registrationEnabled ?? true); const demoMode = useConfigStore((s) => s.config?.demoMode ?? false); diff --git a/packages/frontend/src/pages/Missions.tsx b/packages/frontend/src/pages/Missions.tsx index c4051cb..efcf751 100644 --- a/packages/frontend/src/pages/Missions.tsx +++ b/packages/frontend/src/pages/Missions.tsx @@ -1,13 +1,14 @@ import type { Mission } from "@presto/shared"; import { Briefcase } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { Header } from "@/components/layout/Header"; import { Badge } from "@/components/ui/Badge"; -import { Button } from "@/components/ui/Button"; +import { Button } from "@/components/ui/button"; import { DeleteIconButton } from "@/components/ui/DeleteIconButton"; import { FilterChips } from "@/components/ui/FilterChips"; -import { Input } from "@/components/ui/Input"; +import { Input } from "@/components/ui/input"; import { Modal } from "@/components/ui/Modal"; import { Select } from "@/components/ui/Select"; import { Skeleton } from "@/components/ui/Skeleton"; @@ -16,7 +17,6 @@ import { useClients } from "@/hooks/use-clients"; import { useCompanies } from "@/hooks/use-companies"; import { useDeleteWithFkGuard } from "@/hooks/use-delete-with-fk-guard"; import { useCreateMission, useDeleteMission, useMissions, useUpdateMission } from "@/hooks/use-missions"; -import { useT } from "@/i18n"; import { formatCurrency } from "@/lib/utils"; export function Missions() { @@ -27,6 +27,7 @@ export function Missions() { const [clientId, setClientId] = useState(""); const [companyId, setCompanyId] = useState(""); const [dailyRate, setDailyRate] = useState(""); + const [allocatedDays, setAllocatedDays] = useState(""); const [filterClientId, setFilterClientId] = useState(""); const [filterCompanyId, setFilterCompanyId] = useState(""); @@ -36,7 +37,7 @@ export function Missions() { const createMission = useCreateMission(); const updateMission = useUpdateMission(); const deleteMission = useDeleteMission(); - const { t } = useT(); + const { t } = useTranslation(); const { handleDelete, dialog } = useDeleteWithFkGuard(deleteMission, t, { confirmTitle: t("missions.deleteTitle"), @@ -88,6 +89,7 @@ export function Missions() { setClientId(""); setCompanyId(defaultCompanyId); setDailyRate(""); + setAllocatedDays(""); setShowModal(true); }, [defaultCompanyId]); @@ -104,6 +106,7 @@ export function Missions() { setClientId(mission.clientId); setCompanyId(mission.companyId); setDailyRate(mission.dailyRate?.toString() || ""); + setAllocatedDays(mission.allocatedDays?.toString() || ""); setShowModal(true); }; @@ -116,6 +119,7 @@ export function Missions() { clientId, companyId, dailyRate: dailyRate ? parseFloat(dailyRate) : undefined, + allocatedDays: allocatedDays ? parseInt(allocatedDays, 10) : undefined, }; if (editing) { @@ -221,6 +225,15 @@ export function Missions() { ), }, + { + key: "allocatedDays", + header: t("missions.allocatedDays"), + render: (m) => ( + + {m.allocatedDays ? `${m.allocatedDays} ${t("activity.days")}` : "-"} + + ), + }, { key: "status", header: t("missions.status"), @@ -288,6 +301,15 @@ export function Missions() { suffix={clients?.find((c) => c.id === clientId)?.currency} optional /> + setAllocatedDays(e.target.value)} + placeholder="20" + optional + />